poky-contrib/meta/classes-global/buildstats.bbclass
Richard Purdie 2bd5494291 buildstats: Avoid rare UnboundLocalError
In rare cases BUILDNAME can seemingly be None outside of heartbeat events
which leads to UnboundLocalErrors as bsdir and taskdir aren't defined.

Skip the code in these cases rather than generate tracebacks which cause
bitbake server to exit.

(From OE-Core rev: 0f74d804ba0daf7e8bd6481597740b9d89821414)

Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
2025-03-03 18:01:29 +00:00

306 lines
13 KiB
Plaintext

#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: MIT
#
BUILDSTATS_BASE = "${TMPDIR}/buildstats/"
################################################################################
# Build statistics gathering.
#
# The CPU and Time gathering/tracking functions and bbevent inspiration
# were written by Christopher Larson.
#
################################################################################
def get_buildprocess_cputime(pid):
with open("/proc/%d/stat" % pid, "r") as f:
fields = f.readline().rstrip().split()
# 13: utime, 14: stime, 15: cutime, 16: cstime
return sum(int(field) for field in fields[13:16])
def get_process_cputime(pid):
import resource
with open("/proc/%d/stat" % pid, "r") as f:
fields = f.readline().rstrip().split()
stats = {
'utime' : fields[13],
'stime' : fields[14],
'cutime' : fields[15],
'cstime' : fields[16],
}
iostats = {}
if os.path.isfile("/proc/%d/io" % pid):
with open("/proc/%d/io" % pid, "r") as f:
while True:
i = f.readline().strip()
if not i:
break
if not ":" in i:
# one more extra line is appended (empty or containing "0")
# most probably due to race condition in kernel while
# updating IO stats
break
i = i.split(": ")
iostats[i[0]] = i[1]
resources = resource.getrusage(resource.RUSAGE_SELF)
childres = resource.getrusage(resource.RUSAGE_CHILDREN)
return stats, iostats, resources, childres
def get_cputime():
with open("/proc/stat", "r") as f:
fields = f.readline().rstrip().split()[1:]
return sum(int(field) for field in fields)
def set_timedata(var, d, server_time):
d.setVar(var, server_time)
def get_timedata(var, d, end_time):
oldtime = d.getVar(var, False)
if oldtime is None:
return
return end_time - oldtime
def set_buildtimedata(var, d):
import time
time = time.time()
cputime = get_cputime()
proctime = get_buildprocess_cputime(os.getpid())
d.setVar(var, (time, cputime, proctime))
def get_buildtimedata(var, d):
import time
timedata = d.getVar(var, False)
if timedata is None:
return
oldtime, oldcpu, oldproc = timedata
procdiff = get_buildprocess_cputime(os.getpid()) - oldproc
cpudiff = get_cputime() - oldcpu
end_time = time.time()
timediff = end_time - oldtime
if cpudiff > 0:
cpuperc = float(procdiff) * 100 / cpudiff
else:
cpuperc = None
return timediff, cpuperc
def write_task_data(status, logfile, e, d):
with open(os.path.join(logfile), "a") as f:
elapsedtime = get_timedata("__timedata_task", d, e.time)
if elapsedtime:
f.write(d.expand("${PF}: %s\n" % e.task))
f.write(d.expand("Elapsed time: %0.2f seconds\n" % elapsedtime))
cpu, iostats, resources, childres = get_process_cputime(os.getpid())
if cpu:
f.write("utime: %s\n" % cpu['utime'])
f.write("stime: %s\n" % cpu['stime'])
f.write("cutime: %s\n" % cpu['cutime'])
f.write("cstime: %s\n" % cpu['cstime'])
for i in iostats:
f.write("IO %s: %s\n" % (i, iostats[i]))
rusages = ["ru_utime", "ru_stime", "ru_maxrss", "ru_minflt", "ru_majflt", "ru_inblock", "ru_oublock", "ru_nvcsw", "ru_nivcsw"]
for i in rusages:
f.write("rusage %s: %s\n" % (i, getattr(resources, i)))
for i in rusages:
f.write("Child rusage %s: %s\n" % (i, getattr(childres, i)))
if status == "passed":
f.write("Status: PASSED \n")
else:
f.write("Status: FAILED \n")
f.write("Ended: %0.2f \n" % e.time)
def write_host_data(logfile, e, d, type):
import subprocess, os, datetime
# minimum time allowed for each command to run, in seconds
time_threshold = 0.5
limit = 10
# the total number of commands
num_cmds = 0
msg = ""
if type == "interval":
# interval at which data will be logged
interval = d.getVar("BB_HEARTBEAT_EVENT", False)
if interval is None:
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.")
d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0")
return
interval = int(interval)
cmds = d.getVar('BB_LOG_HOST_STAT_CMDS_INTERVAL')
msg = "Host Stats: Collecting data at %d second intervals.\n" % interval
if cmds is None:
d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0")
bb.warn("buildstats: Collecting host data at intervals failed. Set BB_LOG_HOST_STAT_CMDS_INTERVAL=\"command1 ; command2 ; ... \" in conf/local.conf.")
return
if type == "failure":
cmds = d.getVar('BB_LOG_HOST_STAT_CMDS_FAILURE')
msg = "Host Stats: Collecting data on failure.\n"
msg += "Failed at task: " + e.task + "\n"
if cmds is None:
d.setVar("BB_LOG_HOST_STAT_ON_FAILURE", "0")
bb.warn("buildstats: Collecting host data on failure failed. Set BB_LOG_HOST_STAT_CMDS_FAILURE=\"command1 ; command2 ; ... \" in conf/local.conf.")
return
c_san = []
for cmd in cmds.split(";"):
if len(cmd) == 0:
continue
num_cmds += 1
c_san.append(cmd)
if num_cmds == 0:
if type == "interval":
d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0")
if type == "failure":
d.setVar("BB_LOG_HOST_STAT_ON_FAILURE", "0")
return
# return if the interval is not enough to run all commands within the specified BB_HEARTBEAT_EVENT interval
if type == "interval":
limit = interval / num_cmds
if limit <= time_threshold:
d.setVar("BB_LOG_HOST_STAT_ON_INTERVAL", "0")
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.")
return
# set the environment variables
path = d.getVar("PATH")
opath = d.getVar("BB_ORIGENV", False).getVar("PATH")
ospath = os.environ['PATH']
os.environ['PATH'] = path + ":" + opath + ":" + ospath
with open(logfile, "a") as f:
f.write("Event Time: %f\nDate: %s\n" % (e.time, datetime.datetime.now()))
f.write("%s" % msg)
for c in c_san:
try:
output = subprocess.check_output(c.split(), stderr=subprocess.STDOUT, timeout=limit).decode('utf-8')
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as err:
output = "Error running command: %s\n%s\n" % (c, err)
f.write("%s\n%s\n" % (c, output))
# reset the environment
os.environ['PATH'] = ospath
python run_buildstats () {
import bb.build
import bb.event
import time, subprocess, platform
bn = d.getVar('BUILDNAME')
########################################################################
# bitbake fires HeartbeatEvent even before a build has been
# triggered, causing BUILDNAME to be None
########################################################################
if bn is None:
return
bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn)
taskdir = os.path.join(bsdir, d.getVar('PF'))
if isinstance(e, bb.event.HeartbeatEvent):
if bb.utils.to_boolean(d.getVar("BB_LOG_HOST_STAT_ON_INTERVAL")):
bb.utils.mkdirhier(bsdir)
write_host_data(os.path.join(bsdir, "host_stats_interval"), e, d, "interval")
elif isinstance(e, bb.event.BuildStarted):
########################################################################
# If the kernel was not configured to provide I/O statistics, issue
# a one time warning.
########################################################################
if not os.path.isfile("/proc/%d/io" % os.getpid()):
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)")
########################################################################
# at first pass make the buildstats hierarchy and then
# set the buildname
########################################################################
bb.utils.mkdirhier(bsdir)
set_buildtimedata("__timedata_build", d)
build_time = os.path.join(bsdir, "build_stats")
# write start of build into build_time
with open(build_time, "a") as f:
host_info = platform.uname()
f.write("Host Info: ")
for x in host_info:
if x:
f.write(x + " ")
f.write("\n")
f.write("Build Started: %0.2f \n" % d.getVar('__timedata_build', False)[0])
elif isinstance(e, bb.event.BuildCompleted):
build_time = os.path.join(bsdir, "build_stats")
with open(build_time, "a") as f:
########################################################################
# Write build statistics for the build
########################################################################
timedata = get_buildtimedata("__timedata_build", d)
if timedata:
time, cpu = timedata
# write end of build and cpu used into build_time
f.write("Elapsed time: %0.2f seconds \n" % (time))
if cpu:
f.write("CPU usage: %0.1f%% \n" % cpu)
elif isinstance(e, bb.build.TaskStarted):
set_timedata("__timedata_task", d, e.time)
bb.utils.mkdirhier(taskdir)
# write into the task event file the name and start time
with open(os.path.join(taskdir, e.task), "a") as f:
f.write("Event: %s \n" % bb.event.getName(e))
f.write("Started: %0.2f \n" % e.time)
elif isinstance(e, bb.build.TaskSucceeded):
write_task_data("passed", os.path.join(taskdir, e.task), e, d)
if e.task == "do_rootfs":
bs = os.path.join(bsdir, "build_stats")
with open(bs, "a") as f:
rootfs = d.getVar('IMAGE_ROOTFS')
if os.path.isdir(rootfs):
try:
rootfs_size = subprocess.check_output(["du", "-sh", rootfs],
stderr=subprocess.STDOUT).decode('utf-8')
f.write("Uncompressed Rootfs size: %s" % rootfs_size)
except subprocess.CalledProcessError as err:
bb.warn("Failed to get rootfs size: %s" % err.output.decode('utf-8'))
elif isinstance(e, bb.build.TaskFailed):
# Can have a failure before TaskStarted so need to mkdir here too
bb.utils.mkdirhier(taskdir)
write_task_data("failed", os.path.join(taskdir, e.task), e, d)
########################################################################
# Lets make things easier and tell people where the build failed in
# build_status. We do this here because BuildCompleted triggers no
# matter what the status of the build actually is
########################################################################
build_status = os.path.join(bsdir, "build_stats")
with open(build_status, "a") as f:
f.write(d.expand("Failed at: ${PF} at task: %s \n" % e.task))
if bb.utils.to_boolean(d.getVar("BB_LOG_HOST_STAT_ON_FAILURE")):
write_host_data(os.path.join(bsdir, "host_stats_%s_failure" % e.task), e, d, "failure")
}
addhandler run_buildstats
run_buildstats[eventmask] = "bb.event.BuildStarted bb.event.BuildCompleted bb.event.HeartbeatEvent bb.build.TaskStarted bb.build.TaskSucceeded bb.build.TaskFailed"
python runqueue_stats () {
import oe.buildstats
from bb import event, runqueue
# We should not record any samples before the first task has started,
# because that's the first activity shown in the process chart.
# Besides, at that point we are sure that the build variables
# are available that we need to find the output directory.
# The persistent SystemStats is stored in the datastore and
# closed when the build is done.
system_stats = d.getVar('_buildstats_system_stats', False)
if not system_stats and isinstance(e, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted)):
system_stats = oe.buildstats.SystemStats(d)
d.setVar('_buildstats_system_stats', system_stats)
if system_stats:
# Ensure that we sample at important events.
done = isinstance(e, bb.event.BuildCompleted)
if system_stats.sample(e, force=done):
d.setVar('_buildstats_system_stats', system_stats)
if done:
system_stats.close()
d.delVar('_buildstats_system_stats')
}
addhandler runqueue_stats
runqueue_stats[eventmask] = "bb.runqueue.sceneQueueTaskStarted bb.runqueue.runQueueTaskStarted bb.event.HeartbeatEvent bb.event.BuildCompleted bb.event.MonitorDiskEvent"