base.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. # Copyright (c) 2016, Intel Corporation.
  2. #
  3. # This program is free software; you can redistribute it and/or modify it
  4. # under the terms and conditions of the GNU General Public License,
  5. # version 2, as published by the Free Software Foundation.
  6. #
  7. # This program is distributed in the hope it will be useful, but WITHOUT
  8. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  9. # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
  10. # more details.
  11. #
  12. """Build performance test base classes and functionality"""
  13. import glob
  14. import logging
  15. import os
  16. import re
  17. import shutil
  18. import socket
  19. import tempfile
  20. import time
  21. import traceback
  22. import unittest
  23. from datetime import datetime, timedelta
  24. from oeqa.utils.commands import runCmd, get_bb_vars
  25. from oeqa.utils.git import GitError, GitRepo
  26. # Get logger for this module
  27. log = logging.getLogger('build-perf')
  28. class KernelDropCaches(object):
  29. """Container of the functions for dropping kernel caches"""
  30. sudo_passwd = None
  31. @classmethod
  32. def check(cls):
  33. """Check permssions for dropping kernel caches"""
  34. from getpass import getpass
  35. from locale import getdefaultlocale
  36. cmd = ['sudo', '-k', '-n', 'tee', '/proc/sys/vm/drop_caches']
  37. ret = runCmd(cmd, ignore_status=True, data=b'0')
  38. if ret.output.startswith('sudo:'):
  39. pass_str = getpass(
  40. "\nThe script requires sudo access to drop caches between "
  41. "builds (echo 3 > /proc/sys/vm/drop_caches).\n"
  42. "Please enter your sudo password: ")
  43. cls.sudo_passwd = bytes(pass_str, getdefaultlocale()[1])
  44. @classmethod
  45. def drop(cls):
  46. """Drop kernel caches"""
  47. cmd = ['sudo', '-k']
  48. if cls.sudo_passwd:
  49. cmd.append('-S')
  50. input_data = cls.sudo_passwd + b'\n'
  51. else:
  52. cmd.append('-n')
  53. input_data = b''
  54. cmd += ['tee', '/proc/sys/vm/drop_caches']
  55. input_data += b'3'
  56. runCmd(cmd, data=input_data)
  57. def time_cmd(cmd, **kwargs):
  58. """TIme a command"""
  59. with tempfile.NamedTemporaryFile(mode='w+') as tmpf:
  60. timecmd = ['/usr/bin/time', '-v', '-o', tmpf.name]
  61. if isinstance(cmd, str):
  62. timecmd = ' '.join(timecmd) + ' '
  63. timecmd += cmd
  64. # TODO: 'ignore_status' could/should be removed when globalres.log is
  65. # deprecated. The function would just raise an exception, instead
  66. ret = runCmd(timecmd, ignore_status=True, **kwargs)
  67. timedata = tmpf.file.read()
  68. return ret, timedata
  69. class BuildPerfTestResult(unittest.TextTestResult):
  70. """Runner class for executing the individual tests"""
  71. # List of test cases to run
  72. test_run_queue = []
  73. def __init__(self, out_dir, *args, **kwargs):
  74. super(BuildPerfTestResult, self).__init__(*args, **kwargs)
  75. self.out_dir = out_dir
  76. # Get Git parameters
  77. try:
  78. self.repo = GitRepo('.')
  79. except GitError:
  80. self.repo = None
  81. self.git_revision, self.git_branch = self.get_git_revision()
  82. self.hostname = socket.gethostname()
  83. self.start_time = self.elapsed_time = None
  84. self.successes = []
  85. log.info("Using Git branch:revision %s:%s", self.git_branch,
  86. self.git_revision)
  87. def get_git_revision(self):
  88. """Get git branch and revision under testing"""
  89. rev = os.getenv('OE_BUILDPERFTEST_GIT_REVISION')
  90. branch = os.getenv('OE_BUILDPERFTEST_GIT_BRANCH')
  91. if not self.repo and (not rev or not branch):
  92. log.info("The current working directory doesn't seem to be a Git "
  93. "repository clone. You can specify branch and revision "
  94. "used in test results with OE_BUILDPERFTEST_GIT_REVISION "
  95. "and OE_BUILDPERFTEST_GIT_BRANCH environment variables")
  96. else:
  97. if not rev:
  98. rev = self.repo.run_cmd(['rev-parse', 'HEAD'])
  99. if not branch:
  100. try:
  101. # Strip 11 chars, i.e. 'refs/heads' from the beginning
  102. branch = self.repo.run_cmd(['symbolic-ref', 'HEAD'])[11:]
  103. except GitError:
  104. log.debug('Currently on detached HEAD')
  105. branch = None
  106. return str(rev), str(branch)
  107. def addSuccess(self, test):
  108. """Record results from successful tests"""
  109. super(BuildPerfTestResult, self).addSuccess(test)
  110. self.successes.append((test, None))
  111. def startTest(self, test):
  112. """Pre-test hook"""
  113. test.out_dir = self.out_dir
  114. log.info("Executing test %s: %s", test.name, test.shortDescription())
  115. self.stream.write(datetime.now().strftime("[%Y-%m-%d %H:%M:%S] "))
  116. super(BuildPerfTestResult, self).startTest(test)
  117. def startTestRun(self):
  118. """Pre-run hook"""
  119. self.start_time = datetime.utcnow()
  120. def stopTestRun(self):
  121. """Pre-run hook"""
  122. self.elapsed_time = datetime.utcnow() - self.start_time
  123. def all_results(self):
  124. result_map = {'SUCCESS': self.successes,
  125. 'FAIL': self.failures,
  126. 'ERROR': self.errors,
  127. 'EXP_FAIL': self.expectedFailures,
  128. 'UNEXP_SUCCESS': self.unexpectedSuccesses}
  129. for status, tests in result_map.items():
  130. for test in tests:
  131. yield (status, test)
  132. def update_globalres_file(self, filename):
  133. """Write results to globalres csv file"""
  134. # Map test names to time and size columns in globalres
  135. # The tuples represent index and length of times and sizes
  136. # respectively
  137. gr_map = {'test1': ((0, 1), (8, 1)),
  138. 'test12': ((1, 1), (None, None)),
  139. 'test13': ((2, 1), (9, 1)),
  140. 'test2': ((3, 1), (None, None)),
  141. 'test3': ((4, 3), (None, None)),
  142. 'test4': ((7, 1), (10, 2))}
  143. if self.repo:
  144. git_tag_rev = self.repo.run_cmd(['describe', self.git_revision])
  145. else:
  146. git_tag_rev = self.git_revision
  147. values = ['0'] * 12
  148. for status, test in self.all_results():
  149. if status not in ['SUCCESS', 'FAILURE', 'EXP_SUCCESS']:
  150. continue
  151. (t_ind, t_len), (s_ind, s_len) = gr_map[test.name]
  152. if t_ind is not None:
  153. values[t_ind:t_ind + t_len] = test.times
  154. if s_ind is not None:
  155. values[s_ind:s_ind + s_len] = test.sizes
  156. log.debug("Writing globalres log to %s", filename)
  157. with open(filename, 'a') as fobj:
  158. fobj.write('{},{}:{},{},'.format(self.hostname,
  159. self.git_branch,
  160. self.git_revision,
  161. git_tag_rev))
  162. fobj.write(','.join(values) + '\n')
  163. class BuildPerfTestCase(unittest.TestCase):
  164. """Base class for build performance tests"""
  165. SYSRES = 'sysres'
  166. DISKUSAGE = 'diskusage'
  167. def __init__(self, *args, **kwargs):
  168. super(BuildPerfTestCase, self).__init__(*args, **kwargs)
  169. self.name = self._testMethodName
  170. self.out_dir = None
  171. self.start_time = None
  172. self.elapsed_time = None
  173. self.measurements = []
  174. self.bb_vars = get_bb_vars()
  175. # TODO: remove 'times' and 'sizes' arrays when globalres support is
  176. # removed
  177. self.times = []
  178. self.sizes = []
  179. def run(self, *args, **kwargs):
  180. """Run test"""
  181. self.start_time = datetime.now()
  182. super(BuildPerfTestCase, self).run(*args, **kwargs)
  183. self.elapsed_time = datetime.now() - self.start_time
  184. def log_cmd_output(self, cmd):
  185. """Run a command and log it's output"""
  186. cmd_log = os.path.join(self.out_dir, 'commands.log')
  187. with open(cmd_log, 'a') as fobj:
  188. runCmd(cmd, stdout=fobj)
  189. def measure_cmd_resources(self, cmd, name, legend):
  190. """Measure system resource usage of a command"""
  191. def str_time_to_timedelta(strtime):
  192. """Convert time strig from the time utility to timedelta"""
  193. split = strtime.split(':')
  194. hours = int(split[0]) if len(split) > 2 else 0
  195. mins = int(split[-2])
  196. try:
  197. secs, frac = split[-1].split('.')
  198. except:
  199. secs = split[-1]
  200. frac = '0'
  201. secs = int(secs)
  202. microsecs = int(float('0.' + frac) * pow(10, 6))
  203. return timedelta(0, hours*3600 + mins*60 + secs, microsecs)
  204. cmd_str = cmd if isinstance(cmd, str) else ' '.join(cmd)
  205. log.info("Timing command: %s", cmd_str)
  206. cmd_log = os.path.join(self.out_dir, 'commands.log')
  207. with open(cmd_log, 'a') as fobj:
  208. ret, timedata = time_cmd(cmd, stdout=fobj)
  209. if ret.status:
  210. log.error("Time will be reported as 0. Command failed: %s",
  211. ret.status)
  212. etime = timedelta(0)
  213. self._failed = True
  214. else:
  215. match = re.search(r'.*wall clock.*: (?P<etime>.*)\n', timedata)
  216. etime = str_time_to_timedelta(match.group('etime'))
  217. measurement = {'type': self.SYSRES,
  218. 'name': name,
  219. 'legend': legend}
  220. measurement['values'] = {'elapsed_time': etime}
  221. self.measurements.append(measurement)
  222. e_sec = etime.total_seconds()
  223. nlogs = len(glob.glob(self.out_dir + '/results.log*'))
  224. results_log = os.path.join(self.out_dir,
  225. 'results.log.{}'.format(nlogs + 1))
  226. with open(results_log, 'w') as fobj:
  227. fobj.write(timedata)
  228. # Append to 'times' array for globalres log
  229. self.times.append('{:d}:{:02d}:{:.2f}'.format(int(e_sec / 3600),
  230. int((e_sec % 3600) / 60),
  231. e_sec % 60))
  232. def measure_disk_usage(self, path, name, legend):
  233. """Estimate disk usage of a file or directory"""
  234. # TODO: 'ignore_status' could/should be removed when globalres.log is
  235. # deprecated. The function would just raise an exception, instead
  236. ret = runCmd(['du', '-s', path], ignore_status=True)
  237. if ret.status:
  238. log.error("du failed, disk usage will be reported as 0")
  239. size = 0
  240. self._failed = True
  241. else:
  242. size = int(ret.output.split()[0])
  243. log.debug("Size of %s path is %s", path, size)
  244. measurement = {'type': self.DISKUSAGE,
  245. 'name': name,
  246. 'legend': legend}
  247. measurement['values'] = {'size': size}
  248. self.measurements.append(measurement)
  249. # Append to 'sizes' array for globalres log
  250. self.sizes.append(str(size))
  251. def save_buildstats(self):
  252. """Save buildstats"""
  253. shutil.move(self.bb_vars['BUILDSTATS_BASE'],
  254. os.path.join(self.out_dir, 'buildstats-' + self.name))
  255. @staticmethod
  256. def force_rm(path):
  257. """Equivalent of 'rm -rf'"""
  258. if os.path.isfile(path) or os.path.islink(path):
  259. os.unlink(path)
  260. elif os.path.isdir(path):
  261. shutil.rmtree(path)
  262. def rm_tmp(self):
  263. """Cleanup temporary/intermediate files and directories"""
  264. log.debug("Removing temporary and cache files")
  265. for name in ['bitbake.lock', 'conf/sanity_info',
  266. self.bb_vars['TMPDIR']]:
  267. self.force_rm(name)
  268. def rm_sstate(self):
  269. """Remove sstate directory"""
  270. log.debug("Removing sstate-cache")
  271. self.force_rm(self.bb_vars['SSTATE_DIR'])
  272. def rm_cache(self):
  273. """Drop bitbake caches"""
  274. self.force_rm(self.bb_vars['PERSISTENT_DIR'])
  275. @staticmethod
  276. def sync():
  277. """Sync and drop kernel caches"""
  278. log.debug("Syncing and dropping kernel caches""")
  279. KernelDropCaches.drop()
  280. os.sync()
  281. # Wait a bit for all the dirty blocks to be written onto disk
  282. time.sleep(3)
  283. class BuildPerfTestLoader(unittest.TestLoader):
  284. """Test loader for build performance tests"""
  285. sortTestMethodsUsing = None
  286. class BuildPerfTestRunner(unittest.TextTestRunner):
  287. """Test loader for build performance tests"""
  288. sortTestMethodsUsing = None
  289. def __init__(self, out_dir, *args, **kwargs):
  290. super(BuildPerfTestRunner, self).__init__(*args, **kwargs)
  291. self.out_dir = out_dir
  292. def _makeResult(self):
  293. return BuildPerfTestResult(self.out_dir, self.stream, self.descriptions,
  294. self.verbosity)