1*4882a593SmuzhiyunBUILDSTATS_BASE = "${TMPDIR}/buildstats/" 2*4882a593Smuzhiyun 3*4882a593Smuzhiyun################################################################################ 4*4882a593Smuzhiyun# Build statistics gathering. 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun# The CPU and Time gathering/tracking functions and bbevent inspiration 7*4882a593Smuzhiyun# were written by Christopher Larson. 8*4882a593Smuzhiyun# 9*4882a593Smuzhiyun################################################################################ 10*4882a593Smuzhiyun 11*4882a593Smuzhiyundef get_buildprocess_cputime(pid): 12*4882a593Smuzhiyun with open("/proc/%d/stat" % pid, "r") as f: 13*4882a593Smuzhiyun fields = f.readline().rstrip().split() 14*4882a593Smuzhiyun # 13: utime, 14: stime, 15: cutime, 16: cstime 15*4882a593Smuzhiyun return sum(int(field) for field in fields[13:16]) 16*4882a593Smuzhiyun 17*4882a593Smuzhiyundef get_process_cputime(pid): 18*4882a593Smuzhiyun import resource 19*4882a593Smuzhiyun with open("/proc/%d/stat" % pid, "r") as f: 20*4882a593Smuzhiyun fields = f.readline().rstrip().split() 21*4882a593Smuzhiyun stats = { 22*4882a593Smuzhiyun 'utime' : fields[13], 23*4882a593Smuzhiyun 'stime' : fields[14], 24*4882a593Smuzhiyun 'cutime' : fields[15], 25*4882a593Smuzhiyun 'cstime' : fields[16], 26*4882a593Smuzhiyun } 27*4882a593Smuzhiyun iostats = {} 28*4882a593Smuzhiyun if os.path.isfile("/proc/%d/io" % pid): 29*4882a593Smuzhiyun with open("/proc/%d/io" % pid, "r") as f: 30*4882a593Smuzhiyun while True: 31*4882a593Smuzhiyun i = f.readline().strip() 32*4882a593Smuzhiyun if not i: 33*4882a593Smuzhiyun break 34*4882a593Smuzhiyun if not ":" in i: 35*4882a593Smuzhiyun # one more extra line is appended (empty or containing "0") 36*4882a593Smuzhiyun # most probably due to race condition in kernel while 37*4882a593Smuzhiyun # updating IO stats 38*4882a593Smuzhiyun break 39*4882a593Smuzhiyun i = i.split(": ") 40*4882a593Smuzhiyun iostats[i[0]] = i[1] 41*4882a593Smuzhiyun resources = resource.getrusage(resource.RUSAGE_SELF) 42*4882a593Smuzhiyun childres = resource.getrusage(resource.RUSAGE_CHILDREN) 43*4882a593Smuzhiyun return stats, iostats, resources, childres 44*4882a593Smuzhiyun 45*4882a593Smuzhiyundef get_cputime(): 46*4882a593Smuzhiyun with open("/proc/stat", "r") as f: 47*4882a593Smuzhiyun fields = f.readline().rstrip().split()[1:] 48*4882a593Smuzhiyun return sum(int(field) for field in fields) 49*4882a593Smuzhiyun 50*4882a593Smuzhiyundef set_timedata(var, d, server_time): 51*4882a593Smuzhiyun d.setVar(var, server_time) 52*4882a593Smuzhiyun 53*4882a593Smuzhiyundef get_timedata(var, d, end_time): 54*4882a593Smuzhiyun oldtime = d.getVar(var, False) 55*4882a593Smuzhiyun if oldtime is None: 56*4882a593Smuzhiyun return 57*4882a593Smuzhiyun return end_time - oldtime 58*4882a593Smuzhiyun 59*4882a593Smuzhiyundef set_buildtimedata(var, d): 60*4882a593Smuzhiyun import time 61*4882a593Smuzhiyun time = time.time() 62*4882a593Smuzhiyun cputime = get_cputime() 63*4882a593Smuzhiyun proctime = get_buildprocess_cputime(os.getpid()) 64*4882a593Smuzhiyun d.setVar(var, (time, cputime, proctime)) 65*4882a593Smuzhiyun 66*4882a593Smuzhiyundef get_buildtimedata(var, d): 67*4882a593Smuzhiyun import time 68*4882a593Smuzhiyun timedata = d.getVar(var, False) 69*4882a593Smuzhiyun if timedata is None: 70*4882a593Smuzhiyun return 71*4882a593Smuzhiyun oldtime, oldcpu, oldproc = timedata 72*4882a593Smuzhiyun procdiff = get_buildprocess_cputime(os.getpid()) - oldproc 73*4882a593Smuzhiyun cpudiff = get_cputime() - oldcpu 74*4882a593Smuzhiyun end_time = time.time() 75*4882a593Smuzhiyun timediff = end_time - oldtime 76*4882a593Smuzhiyun if cpudiff > 0: 77*4882a593Smuzhiyun cpuperc = float(procdiff) * 100 / cpudiff 78*4882a593Smuzhiyun else: 79*4882a593Smuzhiyun cpuperc = None 80*4882a593Smuzhiyun return timediff, cpuperc 81*4882a593Smuzhiyun 82*4882a593Smuzhiyundef write_task_data(status, logfile, e, d): 83*4882a593Smuzhiyun with open(os.path.join(logfile), "a") as f: 84*4882a593Smuzhiyun elapsedtime = get_timedata("__timedata_task", d, e.time) 85*4882a593Smuzhiyun if elapsedtime: 86*4882a593Smuzhiyun f.write(d.expand("${PF}: %s\n" % e.task)) 87*4882a593Smuzhiyun f.write(d.expand("Elapsed time: %0.2f seconds\n" % elapsedtime)) 88*4882a593Smuzhiyun cpu, iostats, resources, childres = get_process_cputime(os.getpid()) 89*4882a593Smuzhiyun if cpu: 90*4882a593Smuzhiyun f.write("utime: %s\n" % cpu['utime']) 91*4882a593Smuzhiyun f.write("stime: %s\n" % cpu['stime']) 92*4882a593Smuzhiyun f.write("cutime: %s\n" % cpu['cutime']) 93*4882a593Smuzhiyun f.write("cstime: %s\n" % cpu['cstime']) 94*4882a593Smuzhiyun for i in iostats: 95*4882a593Smuzhiyun f.write("IO %s: %s\n" % (i, iostats[i])) 96*4882a593Smuzhiyun rusages = ["ru_utime", "ru_stime", "ru_maxrss", "ru_minflt", "ru_majflt", "ru_inblock", "ru_oublock", "ru_nvcsw", "ru_nivcsw"] 97*4882a593Smuzhiyun for i in rusages: 98*4882a593Smuzhiyun f.write("rusage %s: %s\n" % (i, getattr(resources, i))) 99*4882a593Smuzhiyun for i in rusages: 100*4882a593Smuzhiyun f.write("Child rusage %s: %s\n" % (i, getattr(childres, i))) 101*4882a593Smuzhiyun if status == "passed": 102*4882a593Smuzhiyun f.write("Status: PASSED \n") 103*4882a593Smuzhiyun else: 104*4882a593Smuzhiyun f.write("Status: FAILED \n") 105*4882a593Smuzhiyun f.write("Ended: %0.2f \n" % e.time) 106*4882a593Smuzhiyun 107*4882a593Smuzhiyundef write_host_data(logfile, e, d, type): 108*4882a593Smuzhiyun import subprocess, os, datetime 109*4882a593Smuzhiyun # minimum time allowed for each command to run, in seconds 110*4882a593Smuzhiyun time_threshold = 0.5 111*4882a593Smuzhiyun limit = 10 112*4882a593Smuzhiyun # the total number of commands 113*4882a593Smuzhiyun num_cmds = 0 114*4882a593Smuzhiyun msg = "" 115*4882a593Smuzhiyun if type == "interval": 116*4882a593Smuzhiyun # interval at which data will be logged 117*4882a593Smuzhiyun interval = d.getVar("BB_HEARTBEAT_EVENT", False) 118*4882a593Smuzhiyun if interval is None: 119*4882a593Smuzhiyun bb.warn("buildstats: Collecting host data at intervals failed. Set BB_HEARTBEAT_EVENT=\"<interval>\" in conf/local.conf for the interval at which host data will be logged.") 120*4882a593Smuzhiyun d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0") 121*4882a593Smuzhiyun return 122*4882a593Smuzhiyun interval = int(interval) 123*4882a593Smuzhiyun cmds = d.getVar('BB_LOG_HOST_STAT_CMDS_INTERVAL') 124*4882a593Smuzhiyun msg = "Host Stats: Collecting data at %d second intervals.\n" % interval 125*4882a593Smuzhiyun if cmds is None: 126*4882a593Smuzhiyun d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0") 127*4882a593Smuzhiyun bb.warn("buildstats: Collecting host data at intervals failed. Set BB_LOG_HOST_STAT_CMDS_INTERVAL=\"command1 ; command2 ; ... \" in conf/local.conf.") 128*4882a593Smuzhiyun return 129*4882a593Smuzhiyun if type == "failure": 130*4882a593Smuzhiyun cmds = d.getVar('BB_LOG_HOST_STAT_CMDS_FAILURE') 131*4882a593Smuzhiyun msg = "Host Stats: Collecting data on failure.\n" 132*4882a593Smuzhiyun msg += "Failed at task: " + e.task + "\n" 133*4882a593Smuzhiyun if cmds is None: 134*4882a593Smuzhiyun d.setVar("BB_LOG_HOST_STAT_ON_FAILURE", "0") 135*4882a593Smuzhiyun bb.warn("buildstats: Collecting host data on failure failed. Set BB_LOG_HOST_STAT_CMDS_FAILURE=\"command1 ; command2 ; ... \" in conf/local.conf.") 136*4882a593Smuzhiyun return 137*4882a593Smuzhiyun c_san = [] 138*4882a593Smuzhiyun for cmd in cmds.split(";"): 139*4882a593Smuzhiyun if len(cmd) == 0: 140*4882a593Smuzhiyun continue 141*4882a593Smuzhiyun num_cmds += 1 142*4882a593Smuzhiyun c_san.append(cmd) 143*4882a593Smuzhiyun if num_cmds == 0: 144*4882a593Smuzhiyun if type == "interval": 145*4882a593Smuzhiyun d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0") 146*4882a593Smuzhiyun if type == "failure": 147*4882a593Smuzhiyun d.setVar("BB_LOG_HOST_STAT_ON_FAILURE", "0") 148*4882a593Smuzhiyun return 149*4882a593Smuzhiyun 150*4882a593Smuzhiyun # return if the interval is not enough to run all commands within the specified BB_HEARTBEAT_EVENT interval 151*4882a593Smuzhiyun if type == "interval": 152*4882a593Smuzhiyun limit = interval / num_cmds 153*4882a593Smuzhiyun if limit <= time_threshold: 154*4882a593Smuzhiyun d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0") 155*4882a593Smuzhiyun bb.warn("buildstats: Collecting host data failed. BB_HEARTBEAT_EVENT interval not enough to run the specified commands. Increase value of BB_HEARTBEAT_EVENT in conf/local.conf.") 156*4882a593Smuzhiyun return 157*4882a593Smuzhiyun 158*4882a593Smuzhiyun # set the environment variables 159*4882a593Smuzhiyun path = d.getVar("PATH") 160*4882a593Smuzhiyun opath = d.getVar("BB_ORIGENV", False).getVar("PATH") 161*4882a593Smuzhiyun ospath = os.environ['PATH'] 162*4882a593Smuzhiyun os.environ['PATH'] = path + ":" + opath + ":" + ospath 163*4882a593Smuzhiyun with open(logfile, "a") as f: 164*4882a593Smuzhiyun f.write("Event Time: %f\nDate: %s\n" % (e.time, datetime.datetime.now())) 165*4882a593Smuzhiyun f.write("%s" % msg) 166*4882a593Smuzhiyun for c in c_san: 167*4882a593Smuzhiyun try: 168*4882a593Smuzhiyun output = subprocess.check_output(c.split(), stderr=subprocess.STDOUT, timeout=limit).decode('utf-8') 169*4882a593Smuzhiyun except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as err: 170*4882a593Smuzhiyun output = "Error running command: %s\n%s\n" % (c, err) 171*4882a593Smuzhiyun f.write("%s\n%s\n" % (c, output)) 172*4882a593Smuzhiyun # reset the environment 173*4882a593Smuzhiyun os.environ['PATH'] = ospath 174*4882a593Smuzhiyun 175*4882a593Smuzhiyunpython run_buildstats () { 176*4882a593Smuzhiyun import bb.build 177*4882a593Smuzhiyun import bb.event 178*4882a593Smuzhiyun import time, subprocess, platform 179*4882a593Smuzhiyun 180*4882a593Smuzhiyun bn = d.getVar('BUILDNAME') 181*4882a593Smuzhiyun ######################################################################## 182*4882a593Smuzhiyun # bitbake fires HeartbeatEvent even before a build has been 183*4882a593Smuzhiyun # triggered, causing BUILDNAME to be None 184*4882a593Smuzhiyun ######################################################################## 185*4882a593Smuzhiyun if bn is not None: 186*4882a593Smuzhiyun bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn) 187*4882a593Smuzhiyun taskdir = os.path.join(bsdir, d.getVar('PF')) 188*4882a593Smuzhiyun if isinstance(e, bb.event.HeartbeatEvent) and bb.utils.to_boolean(d.getVar("BB_LOG_HOST_STAT_ON_INTERVAL")): 189*4882a593Smuzhiyun bb.utils.mkdirhier(bsdir) 190*4882a593Smuzhiyun write_host_data(os.path.join(bsdir, "host_stats_interval"), e, d, "interval") 191*4882a593Smuzhiyun 192*4882a593Smuzhiyun if isinstance(e, bb.event.BuildStarted): 193*4882a593Smuzhiyun ######################################################################## 194*4882a593Smuzhiyun # If the kernel was not configured to provide I/O statistics, issue 195*4882a593Smuzhiyun # a one time warning. 196*4882a593Smuzhiyun ######################################################################## 197*4882a593Smuzhiyun if not os.path.isfile("/proc/%d/io" % os.getpid()): 198*4882a593Smuzhiyun bb.warn("The Linux kernel on your build host was not configured to provide process I/O statistics. (CONFIG_TASK_IO_ACCOUNTING is not set)") 199*4882a593Smuzhiyun 200*4882a593Smuzhiyun ######################################################################## 201*4882a593Smuzhiyun # at first pass make the buildstats hierarchy and then 202*4882a593Smuzhiyun # set the buildname 203*4882a593Smuzhiyun ######################################################################## 204*4882a593Smuzhiyun bb.utils.mkdirhier(bsdir) 205*4882a593Smuzhiyun set_buildtimedata("__timedata_build", d) 206*4882a593Smuzhiyun build_time = os.path.join(bsdir, "build_stats") 207*4882a593Smuzhiyun # write start of build into build_time 208*4882a593Smuzhiyun with open(build_time, "a") as f: 209*4882a593Smuzhiyun host_info = platform.uname() 210*4882a593Smuzhiyun f.write("Host Info: ") 211*4882a593Smuzhiyun for x in host_info: 212*4882a593Smuzhiyun if x: 213*4882a593Smuzhiyun f.write(x + " ") 214*4882a593Smuzhiyun f.write("\n") 215*4882a593Smuzhiyun f.write("Build Started: %0.2f \n" % d.getVar('__timedata_build', False)[0]) 216*4882a593Smuzhiyun 217*4882a593Smuzhiyun elif isinstance(e, bb.event.BuildCompleted): 218*4882a593Smuzhiyun build_time = os.path.join(bsdir, "build_stats") 219*4882a593Smuzhiyun with open(build_time, "a") as f: 220*4882a593Smuzhiyun ######################################################################## 221*4882a593Smuzhiyun # Write build statistics for the build 222*4882a593Smuzhiyun ######################################################################## 223*4882a593Smuzhiyun timedata = get_buildtimedata("__timedata_build", d) 224*4882a593Smuzhiyun if timedata: 225*4882a593Smuzhiyun time, cpu = timedata 226*4882a593Smuzhiyun # write end of build and cpu used into build_time 227*4882a593Smuzhiyun f.write("Elapsed time: %0.2f seconds \n" % (time)) 228*4882a593Smuzhiyun if cpu: 229*4882a593Smuzhiyun f.write("CPU usage: %0.1f%% \n" % cpu) 230*4882a593Smuzhiyun 231*4882a593Smuzhiyun if isinstance(e, bb.build.TaskStarted): 232*4882a593Smuzhiyun set_timedata("__timedata_task", d, e.time) 233*4882a593Smuzhiyun bb.utils.mkdirhier(taskdir) 234*4882a593Smuzhiyun # write into the task event file the name and start time 235*4882a593Smuzhiyun with open(os.path.join(taskdir, e.task), "a") as f: 236*4882a593Smuzhiyun f.write("Event: %s \n" % bb.event.getName(e)) 237*4882a593Smuzhiyun f.write("Started: %0.2f \n" % e.time) 238*4882a593Smuzhiyun 239*4882a593Smuzhiyun elif isinstance(e, bb.build.TaskSucceeded): 240*4882a593Smuzhiyun write_task_data("passed", os.path.join(taskdir, e.task), e, d) 241*4882a593Smuzhiyun if e.task == "do_rootfs": 242*4882a593Smuzhiyun bs = os.path.join(bsdir, "build_stats") 243*4882a593Smuzhiyun with open(bs, "a") as f: 244*4882a593Smuzhiyun rootfs = d.getVar('IMAGE_ROOTFS') 245*4882a593Smuzhiyun if os.path.isdir(rootfs): 246*4882a593Smuzhiyun try: 247*4882a593Smuzhiyun rootfs_size = subprocess.check_output(["du", "-sh", rootfs], 248*4882a593Smuzhiyun stderr=subprocess.STDOUT).decode('utf-8') 249*4882a593Smuzhiyun f.write("Uncompressed Rootfs size: %s" % rootfs_size) 250*4882a593Smuzhiyun except subprocess.CalledProcessError as err: 251*4882a593Smuzhiyun bb.warn("Failed to get rootfs size: %s" % err.output.decode('utf-8')) 252*4882a593Smuzhiyun 253*4882a593Smuzhiyun elif isinstance(e, bb.build.TaskFailed): 254*4882a593Smuzhiyun # Can have a failure before TaskStarted so need to mkdir here too 255*4882a593Smuzhiyun bb.utils.mkdirhier(taskdir) 256*4882a593Smuzhiyun write_task_data("failed", os.path.join(taskdir, e.task), e, d) 257*4882a593Smuzhiyun ######################################################################## 258*4882a593Smuzhiyun # Lets make things easier and tell people where the build failed in 259*4882a593Smuzhiyun # build_status. We do this here because BuildCompleted triggers no 260*4882a593Smuzhiyun # matter what the status of the build actually is 261*4882a593Smuzhiyun ######################################################################## 262*4882a593Smuzhiyun build_status = os.path.join(bsdir, "build_stats") 263*4882a593Smuzhiyun with open(build_status, "a") as f: 264*4882a593Smuzhiyun f.write(d.expand("Failed at: ${PF} at task: %s \n" % e.task)) 265*4882a593Smuzhiyun if bb.utils.to_boolean(d.getVar("BB_LOG_HOST_STAT_ON_FAILURE")): 266*4882a593Smuzhiyun write_host_data(os.path.join(bsdir, "host_stats_%s_failure" % e.task), e, d, "failure") 267*4882a593Smuzhiyun} 268*4882a593Smuzhiyun 269*4882a593Smuzhiyunaddhandler run_buildstats 270*4882a593Smuzhiyunrun_buildstats[eventmask] = "bb.event.BuildStarted bb.event.BuildCompleted bb.event.HeartbeatEvent bb.build.TaskStarted bb.build.TaskSucceeded bb.build.TaskFailed" 271*4882a593Smuzhiyun 272*4882a593Smuzhiyunpython runqueue_stats () { 273*4882a593Smuzhiyun import buildstats 274*4882a593Smuzhiyun from bb import event, runqueue 275*4882a593Smuzhiyun # We should not record any samples before the first task has started, 276*4882a593Smuzhiyun # because that's the first activity shown in the process chart. 277*4882a593Smuzhiyun # Besides, at that point we are sure that the build variables 278*4882a593Smuzhiyun # are available that we need to find the output directory. 279*4882a593Smuzhiyun # The persistent SystemStats is stored in the datastore and 280*4882a593Smuzhiyun # closed when the build is done. 281*4882a593Smuzhiyun system_stats = d.getVar('_buildstats_system_stats', False) 282*4882a593Smuzhiyun if not system_stats and isinstance(e, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted)): 283*4882a593Smuzhiyun system_stats = buildstats.SystemStats(d) 284*4882a593Smuzhiyun d.setVar('_buildstats_system_stats', system_stats) 285*4882a593Smuzhiyun if system_stats: 286*4882a593Smuzhiyun # Ensure that we sample at important events. 287*4882a593Smuzhiyun done = isinstance(e, bb.event.BuildCompleted) 288*4882a593Smuzhiyun system_stats.sample(e, force=done) 289*4882a593Smuzhiyun if done: 290*4882a593Smuzhiyun system_stats.close() 291*4882a593Smuzhiyun d.delVar('_buildstats_system_stats') 292*4882a593Smuzhiyun} 293*4882a593Smuzhiyun 294*4882a593Smuzhiyunaddhandler runqueue_stats 295*4882a593Smuzhiyunrunqueue_stats[eventmask] = "bb.runqueue.sceneQueueTaskStarted bb.runqueue.runQueueTaskStarted bb.event.HeartbeatEvent bb.event.BuildCompleted bb.event.MonitorDiskEvent" 296