mirror of
git://git.yoctoproject.org/poky.git
synced 2025-08-22 00:42:05 +02:00

host data, for both interval and failure, was previously logged into the same file which was difficult to read as the files file were usually large. host data is now logged into separate files, for each type of logging (failure and interval) and also for each failed task making it easier to read/parse. (From OE-Core rev: 1a0fb3c0794f4e66086e567a297b4d9379c6b8f3) Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
296 lines
13 KiB
Plaintext
296 lines
13 KiB
Plaintext
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 not None:
|
|
bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn)
|
|
taskdir = os.path.join(bsdir, d.getVar('PF'))
|
|
if isinstance(e, bb.event.HeartbeatEvent) and 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")
|
|
|
|
if 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)
|
|
|
|
if 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 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 = 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)
|
|
system_stats.sample(e, force=done)
|
|
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"
|