1#!/usr/bin/env python3 2# ignore-tidy-linelength 3 4# This is a small script that we use on CI to collect CPU usage statistics of 5# our builders. By seeing graphs of CPU usage over time we hope to correlate 6# that with possible improvements to Rust's own build system, ideally diagnosing 7# that either builders are always fully using their CPU resources or they're 8# idle for long stretches of time. 9# 10# This script is relatively simple, but it's platform specific. Each platform 11# (OSX/Windows/Linux) has a different way of calculating the current state of 12# CPU at a point in time. We then compare two captured states to determine the 13# percentage of time spent in one state versus another. The state capturing is 14# all platform-specific but the loop at the bottom is the cross platform part 15# that executes everywhere. 16# 17# # Viewing statistics 18# 19# All builders will upload their CPU statistics as CSV files to our S3 buckets. 20# These URLS look like: 21# 22# https://$bucket.s3.amazonaws.com/rustc-builds/$commit/cpu-$builder.csv 23# 24# for example 25# 26# https://rust-lang-ci2.s3.amazonaws.com/rustc-builds/68baada19cd5340f05f0db15a3e16d6671609bcc/cpu-x86_64-apple.csv 27# 28# Each CSV file has two columns. The first is the timestamp of the measurement 29# and the second column is the % of idle cpu time in that time slice. Ideally 30# the second column is always zero. 31# 32# Once you've downloaded a file there's various ways to plot it and visualize 33# it. For command line usage you use the `src/etc/cpu-usage-over-time-plot.sh` 34# script in this repository. 35 36import datetime 37import sys 38import time 39 40# Python 3.3 changed the value of `sys.platform` on Linux from "linux2" to just 41# "linux". We check here with `.startswith` to keep compatibility with older 42# Python versions (especially Python 2.7). 43if sys.platform.startswith('linux'): 44 class State: 45 def __init__(self): 46 with open('/proc/stat', 'r') as file: 47 data = file.readline().split() 48 if data[0] != 'cpu': 49 raise Exception('did not start with "cpu"') 50 self.user = int(data[1]) 51 self.nice = int(data[2]) 52 self.system = int(data[3]) 53 self.idle = int(data[4]) 54 self.iowait = int(data[5]) 55 self.irq = int(data[6]) 56 self.softirq = int(data[7]) 57 self.steal = int(data[8]) 58 self.guest = int(data[9]) 59 self.guest_nice = int(data[10]) 60 61 def idle_since(self, prev): 62 user = self.user - prev.user 63 nice = self.nice - prev.nice 64 system = self.system - prev.system 65 idle = self.idle - prev.idle 66 iowait = self.iowait - prev.iowait 67 irq = self.irq - prev.irq 68 softirq = self.softirq - prev.softirq 69 steal = self.steal - prev.steal 70 guest = self.guest - prev.guest 71 guest_nice = self.guest_nice - prev.guest_nice 72 total = user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice 73 return float(idle) / float(total) * 100 74 75elif sys.platform == 'win32': 76 from ctypes.wintypes import DWORD 77 from ctypes import Structure, windll, WinError, GetLastError, byref 78 79 class FILETIME(Structure): 80 _fields_ = [ 81 ("dwLowDateTime", DWORD), 82 ("dwHighDateTime", DWORD), 83 ] 84 85 class State: 86 def __init__(self): 87 idle, kernel, user = FILETIME(), FILETIME(), FILETIME() 88 89 success = windll.kernel32.GetSystemTimes( 90 byref(idle), 91 byref(kernel), 92 byref(user), 93 ) 94 95 assert success, WinError(GetLastError())[1] 96 97 self.idle = (idle.dwHighDateTime << 32) | idle.dwLowDateTime 98 self.kernel = (kernel.dwHighDateTime << 32) | kernel.dwLowDateTime 99 self.user = (user.dwHighDateTime << 32) | user.dwLowDateTime 100 101 def idle_since(self, prev): 102 idle = self.idle - prev.idle 103 user = self.user - prev.user 104 kernel = self.kernel - prev.kernel 105 return float(idle) / float(user + kernel) * 100 106 107elif sys.platform == 'darwin': 108 from ctypes import * 109 libc = cdll.LoadLibrary('/usr/lib/libc.dylib') 110 111 class host_cpu_load_info_data_t(Structure): 112 _fields_ = [("cpu_ticks", c_uint * 4)] 113 114 host_statistics = libc.host_statistics 115 host_statistics.argtypes = [ 116 c_uint, 117 c_int, 118 POINTER(host_cpu_load_info_data_t), 119 POINTER(c_int) 120 ] 121 host_statistics.restype = c_int 122 123 CPU_STATE_USER = 0 124 CPU_STATE_SYSTEM = 1 125 CPU_STATE_IDLE = 2 126 CPU_STATE_NICE = 3 127 class State: 128 def __init__(self): 129 stats = host_cpu_load_info_data_t() 130 count = c_int(4) # HOST_CPU_LOAD_INFO_COUNT 131 err = libc.host_statistics( 132 libc.mach_host_self(), 133 c_int(3), # HOST_CPU_LOAD_INFO 134 byref(stats), 135 byref(count), 136 ) 137 assert err == 0 138 self.system = stats.cpu_ticks[CPU_STATE_SYSTEM] 139 self.user = stats.cpu_ticks[CPU_STATE_USER] 140 self.idle = stats.cpu_ticks[CPU_STATE_IDLE] 141 self.nice = stats.cpu_ticks[CPU_STATE_NICE] 142 143 def idle_since(self, prev): 144 user = self.user - prev.user 145 system = self.system - prev.system 146 idle = self.idle - prev.idle 147 nice = self.nice - prev.nice 148 return float(idle) / float(user + system + idle + nice) * 100.0 149 150else: 151 print('unknown platform', sys.platform) 152 sys.exit(1) 153 154cur_state = State() 155print("Time,Idle") 156while True: 157 time.sleep(1) 158 next_state = State() 159 now = datetime.datetime.utcnow().isoformat() 160 idle = next_state.idle_since(cur_state) 161 print("%s,%s" % (now, idle)) 162 sys.stdout.flush() 163 cur_state = next_state 164