blob: d63cff532ee477aeb1b2d4310ddae78acff72dee [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# Toaster helper class
3#
4# Copyright (C) 2013 Intel Corporation
5#
6# Released under the MIT license (see COPYING.MIT)
7#
8# This bbclass is designed to extract data used by OE-Core during the build process,
9# for recording in the Toaster system.
10# The data access is synchronous, preserving the build data integrity across
11# different builds.
12#
13# The data is transferred through the event system, using the MetadataEvent objects.
14#
15# The model is to enable the datadump functions as postfuncs, and have the dump
16# executed after the real taskfunc has been executed. This prevents task signature changing
17# is toaster is enabled or not. Build performance is not affected if Toaster is not enabled.
18#
19# To enable, use INHERIT in local.conf:
20#
21# INHERIT += "toaster"
22#
23#
24#
25#
26
27# Find and dump layer info when we got the layers parsed
28
29
30
31python toaster_layerinfo_dumpdata() {
32 import subprocess
33
34 def _get_git_branch(layer_path):
35 branch = subprocess.Popen("git symbolic-ref HEAD 2>/dev/null ", cwd=layer_path, shell=True, stdout=subprocess.PIPE).communicate()[0]
36 branch = branch.replace('refs/heads/', '').rstrip()
37 return branch
38
39 def _get_git_revision(layer_path):
40 revision = subprocess.Popen("git rev-parse HEAD 2>/dev/null ", cwd=layer_path, shell=True, stdout=subprocess.PIPE).communicate()[0].rstrip()
41 return revision
42
43 def _get_url_map_name(layer_name):
44 """ Some layers have a different name on openembedded.org site,
45 this method returns the correct name to use in the URL
46 """
47
48 url_name = layer_name
49 url_mapping = {'meta': 'openembedded-core'}
50
51 for key in url_mapping.keys():
52 if key == layer_name:
53 url_name = url_mapping[key]
54
55 return url_name
56
57 def _get_layer_version_information(layer_path):
58
59 layer_version_info = {}
60 layer_version_info['branch'] = _get_git_branch(layer_path)
61 layer_version_info['commit'] = _get_git_revision(layer_path)
62 layer_version_info['priority'] = 0
63
64 return layer_version_info
65
66
67 def _get_layer_dict(layer_path):
68
69 layer_info = {}
70 layer_name = layer_path.split('/')[-1]
71 layer_url = 'http://layers.openembedded.org/layerindex/layer/{layer}/'
72 layer_url_name = _get_url_map_name(layer_name)
73
74 layer_info['name'] = layer_url_name
75 layer_info['local_path'] = layer_path
76 layer_info['layer_index_url'] = layer_url.format(layer=layer_url_name)
77 layer_info['version'] = _get_layer_version_information(layer_path)
78
79 return layer_info
80
81
82 bblayers = e.data.getVar("BBLAYERS", True)
83
84 llayerinfo = {}
85
86 for layer in { l for l in bblayers.strip().split(" ") if len(l) }:
87 llayerinfo[layer] = _get_layer_dict(layer)
88
89
90 bb.event.fire(bb.event.MetadataEvent("LayerInfo", llayerinfo), e.data)
91}
92
93# Dump package file info data
94
95def _toaster_load_pkgdatafile(dirpath, filepath):
96 import json
97 import re
98 pkgdata = {}
99 with open(os.path.join(dirpath, filepath), "r") as fin:
100 for line in fin:
101 try:
102 kn, kv = line.strip().split(": ", 1)
103 m = re.match(r"^PKG_([^A-Z:]*)", kn)
104 if m:
105 pkgdata['OPKGN'] = m.group(1)
106 kn = "_".join([x for x in kn.split("_") if x.isupper()])
107 pkgdata[kn] = kv.strip()
108 if kn == 'FILES_INFO':
109 pkgdata[kn] = json.loads(kv)
110
111 except ValueError:
112 pass # ignore lines without valid key: value pairs
113 return pkgdata
114
115
116python toaster_package_dumpdata() {
117 """
118 Dumps the data created by emit_pkgdata
119 """
120 # replicate variables from the package.bbclass
121
122 packages = d.getVar('PACKAGES', True)
123 pkgdest = d.getVar('PKGDEST', True)
124
125 pkgdatadir = d.getVar('PKGDESTWORK', True)
126
127 # scan and send data for each package
128
129 lpkgdata = {}
130 for pkg in packages.split():
131
132 lpkgdata = _toaster_load_pkgdatafile(pkgdatadir + "/runtime/", pkg)
133
134 # Fire an event containing the pkg data
135 bb.event.fire(bb.event.MetadataEvent("SinglePackageInfo", lpkgdata), d)
136}
137
138# 2. Dump output image files information
139
140python toaster_image_dumpdata() {
141 """
142 Image filename for output images is not standardized.
143 image_types.bbclass will spell out IMAGE_CMD_xxx variables that actually
144 have hardcoded ways to create image file names in them.
145 So we look for files starting with the set name.
146 """
147
148 deploy_dir_image = d.getVar('DEPLOY_DIR_IMAGE', True);
149 image_name = d.getVar('IMAGE_NAME', True);
150
151 image_info_data = {}
152 artifact_info_data = {}
153
154 # collect all artifacts
155 for dirpath, dirnames, filenames in os.walk(deploy_dir_image):
156 for fn in filenames:
157 try:
158 if fn.startswith(image_name):
159 image_output = os.path.join(dirpath, fn)
160 image_info_data[image_output] = os.stat(image_output).st_size
161 else:
162 import stat
163 artifact_path = os.path.join(dirpath, fn)
164 filestat = os.stat(artifact_path)
165 if not os.path.islink(artifact_path):
166 artifact_info_data[artifact_path] = filestat.st_size
167 except OSError as e:
168 bb.event.fire(bb.event.MetadataEvent("OSErrorException", e), d)
169
170 bb.event.fire(bb.event.MetadataEvent("ImageFileSize",image_info_data), d)
171 bb.event.fire(bb.event.MetadataEvent("ArtifactFileSize",artifact_info_data), d)
172}
173
174
175
176# collect list of buildstats files based on fired events; when the build completes, collect all stats and fire an event with collected data
177
178python toaster_collect_task_stats() {
179 import bb.build
180 import bb.event
181 import bb.data
182 import bb.utils
183 import os
184
185 if not e.data.getVar('BUILDSTATS_BASE', True):
186 return # if we don't have buildstats, we cannot collect stats
187
188 def _append_read_list(v):
189 lock = bb.utils.lockfile(e.data.expand("${TOPDIR}/toaster.lock"), False, True)
190
191 with open(os.path.join(e.data.getVar('BUILDSTATS_BASE', True), "toasterstatlist"), "a") as fout:
192 bn = get_bn(e)
193 bsdir = os.path.join(e.data.getVar('BUILDSTATS_BASE', True), bn)
194 taskdir = os.path.join(bsdir, e.data.expand("${PF}"))
195 fout.write("%s::%s::%s::%s\n" % (e.taskfile, e.taskname, os.path.join(taskdir, e.task), e.data.expand("${PN}")))
196
197 bb.utils.unlockfile(lock)
198
199 def _read_stats(filename):
200 cpu_usage = 0
201 disk_io = 0
202 startio = '0'
203 endio = '0'
204 started = '0'
205 ended = '0'
206 pn = ''
207 taskname = ''
208 statinfo = {}
209
210 with open(filename, 'r') as task_bs:
211 for line in task_bs.readlines():
212 k,v = line.strip().split(": ", 1)
213 statinfo[k] = v
214
215 if "CPU usage" in statinfo:
216 cpu_usage = str(statinfo["CPU usage"]).strip('% \n\r')
217
218 if "EndTimeIO" in statinfo:
219 endio = str(statinfo["EndTimeIO"]).strip('% \n\r')
220
221 if "StartTimeIO" in statinfo:
222 startio = str(statinfo["StartTimeIO"]).strip('% \n\r')
223
224 if "Started" in statinfo:
225 started = str(statinfo["Started"]).strip('% \n\r')
226
227 if "Ended" in statinfo:
228 ended = str(statinfo["Ended"]).strip('% \n\r')
229
230 disk_io = int(endio) - int(startio)
231
232 elapsed_time = float(ended) - float(started)
233
234 cpu_usage = float(cpu_usage)
235
236 return {'cpu_usage': cpu_usage, 'disk_io': disk_io, 'elapsed_time': elapsed_time}
237
238
239 if isinstance(e, (bb.build.TaskSucceeded, bb.build.TaskFailed)):
240 _append_read_list(e)
241 pass
242
243
244 if isinstance(e, bb.event.BuildCompleted) and os.path.exists(os.path.join(e.data.getVar('BUILDSTATS_BASE', True), "toasterstatlist")):
245 events = []
246 with open(os.path.join(e.data.getVar('BUILDSTATS_BASE', True), "toasterstatlist"), "r") as fin:
247 for line in fin:
248 (taskfile, taskname, filename, recipename) = line.strip().split("::")
249 events.append((taskfile, taskname, _read_stats(filename), recipename))
250 bb.event.fire(bb.event.MetadataEvent("BuildStatsList", events), e.data)
251 os.unlink(os.path.join(e.data.getVar('BUILDSTATS_BASE', True), "toasterstatlist"))
252}
253
254# dump relevant build history data as an event when the build is completed
255
256python toaster_buildhistory_dump() {
257 import re
258 BUILDHISTORY_DIR = e.data.expand("${TOPDIR}/buildhistory")
259 BUILDHISTORY_DIR_IMAGE_BASE = e.data.expand("%s/images/${MACHINE_ARCH}/${TCLIBC}/"% BUILDHISTORY_DIR)
260 pkgdata_dir = e.data.getVar("PKGDATA_DIR", True)
261
262
263 # scan the build targets for this build
264 images = {}
265 allpkgs = {}
266 files = {}
267 for target in e._pkgs:
268 installed_img_path = e.data.expand(os.path.join(BUILDHISTORY_DIR_IMAGE_BASE, target))
269 if os.path.exists(installed_img_path):
270 images[target] = {}
271 files[target] = {}
272 files[target]['dirs'] = []
273 files[target]['syms'] = []
274 files[target]['files'] = []
275 with open("%s/installed-package-sizes.txt" % installed_img_path, "r") as fin:
276 for line in fin:
277 line = line.rstrip(";")
278 psize, px = line.split("\t")
279 punit, pname = px.split(" ")
280 # this size is "installed-size" as it measures how much space it takes on disk
281 images[target][pname.strip()] = {'size':int(psize)*1024, 'depends' : []}
282
283 with open("%s/depends.dot" % installed_img_path, "r") as fin:
284 p = re.compile(r' -> ')
285 dot = re.compile(r'.*style=dotted')
286 for line in fin:
287 line = line.rstrip(';')
288 linesplit = p.split(line)
289 if len(linesplit) == 2:
290 pname = linesplit[0].rstrip('"').strip('"')
291 dependsname = linesplit[1].split(" ")[0].strip().strip(";").strip('"').rstrip('"')
292 deptype = "depends"
293 if dot.match(line):
294 deptype = "recommends"
295 if not pname in images[target]:
296 images[target][pname] = {'size': 0, 'depends' : []}
297 if not dependsname in images[target]:
298 images[target][dependsname] = {'size': 0, 'depends' : []}
299 images[target][pname]['depends'].append((dependsname, deptype))
300
301 with open("%s/files-in-image.txt" % installed_img_path, "r") as fin:
302 for line in fin:
303 lc = [ x for x in line.strip().split(" ") if len(x) > 0 ]
304 if lc[0].startswith("l"):
305 files[target]['syms'].append(lc)
306 elif lc[0].startswith("d"):
307 files[target]['dirs'].append(lc)
308 else:
309 files[target]['files'].append(lc)
310
311 for pname in images[target]:
312 if not pname in allpkgs:
313 try:
314 pkgdata = _toaster_load_pkgdatafile("%s/runtime-reverse/" % pkgdata_dir, pname)
315 except IOError as err:
316 if err.errno == 2:
317 # We expect this e.g. for RRECOMMENDS that are unsatisfied at runtime
318 continue
319 else:
320 raise
321 allpkgs[pname] = pkgdata
322
323
324 data = { 'pkgdata' : allpkgs, 'imgdata' : images, 'filedata' : files }
325
326 bb.event.fire(bb.event.MetadataEvent("ImagePkgList", data), e.data)
327
328}
329
330# dump information related to license manifest path
331
332python toaster_licensemanifest_dump() {
333 deploy_dir = d.getVar('DEPLOY_DIR', True);
334 image_name = d.getVar('IMAGE_NAME', True);
335
336 data = { 'deploy_dir' : deploy_dir, 'image_name' : image_name }
337
338 bb.event.fire(bb.event.MetadataEvent("LicenseManifestPath", data), d)
339}
340
341# set event handlers
342addhandler toaster_layerinfo_dumpdata
343toaster_layerinfo_dumpdata[eventmask] = "bb.event.TreeDataPreparationCompleted"
344
345addhandler toaster_collect_task_stats
346toaster_collect_task_stats[eventmask] = "bb.event.BuildCompleted bb.build.TaskSucceeded bb.build.TaskFailed"
347
348addhandler toaster_buildhistory_dump
349toaster_buildhistory_dump[eventmask] = "bb.event.BuildCompleted"
350do_package[postfuncs] += "toaster_package_dumpdata "
351do_package[vardepsexclude] += "toaster_package_dumpdata "
352
353do_rootfs[postfuncs] += "toaster_image_dumpdata "
354do_rootfs[postfuncs] += "toaster_licensemanifest_dump "
355do_rootfs[vardepsexclude] += "toaster_image_dumpdata toaster_licensemanifest_dump"