buildstats.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # Implements system state sampling. Called by buildstats.bbclass.
  2. # Because it is a real Python module, it can hold persistent state,
  3. # like open log files and the time of the last sampling.
  4. import time
  5. import re
  6. import bb.event
  7. class SystemStats:
  8. def __init__(self, d):
  9. bn = d.getVar('BUILDNAME')
  10. bsdir = os.path.join(d.getVar('BUILDSTATS_BASE'), bn)
  11. bb.utils.mkdirhier(bsdir)
  12. self.proc_files = []
  13. for filename, handler in (
  14. ('diskstats', self._reduce_diskstats),
  15. ('meminfo', self._reduce_meminfo),
  16. ('stat', self._reduce_stat),
  17. ):
  18. # The corresponding /proc files might not exist on the host.
  19. # For example, /proc/diskstats is not available in virtualized
  20. # environments like Linux-VServer. Silently skip collecting
  21. # the data.
  22. if os.path.exists(os.path.join('/proc', filename)):
  23. # In practice, this class gets instantiated only once in
  24. # the bitbake cooker process. Therefore 'append' mode is
  25. # not strictly necessary, but using it makes the class
  26. # more robust should two processes ever write
  27. # concurrently.
  28. destfile = os.path.join(bsdir, '%sproc_%s.log' % ('reduced_' if handler else '', filename))
  29. self.proc_files.append((filename, open(destfile, 'ab'), handler))
  30. self.monitor_disk = open(os.path.join(bsdir, 'monitor_disk.log'), 'ab')
  31. # Last time that we sampled /proc data resp. recorded disk monitoring data.
  32. self.last_proc = 0
  33. self.last_disk_monitor = 0
  34. # Minimum number of seconds between recording a sample. This
  35. # becames relevant when we get called very often while many
  36. # short tasks get started. Sampling during quiet periods
  37. # depends on the heartbeat event, which fires less often.
  38. self.min_seconds = 1
  39. self.meminfo_regex = re.compile(b'^(MemTotal|MemFree|Buffers|Cached|SwapTotal|SwapFree):\s*(\d+)')
  40. self.diskstats_regex = re.compile(b'^([hsv]d.|mtdblock\d|mmcblk\d|cciss/c\d+d\d+.*)$')
  41. self.diskstats_ltime = None
  42. self.diskstats_data = None
  43. self.stat_ltimes = None
  44. def close(self):
  45. self.monitor_disk.close()
  46. for _, output, _ in self.proc_files:
  47. output.close()
  48. def _reduce_meminfo(self, time, data):
  49. """
  50. Extracts 'MemTotal', 'MemFree', 'Buffers', 'Cached', 'SwapTotal', 'SwapFree'
  51. and writes their values into a single line, in that order.
  52. """
  53. values = {}
  54. for line in data.split(b'\n'):
  55. m = self.meminfo_regex.match(line)
  56. if m:
  57. values[m.group(1)] = m.group(2)
  58. if len(values) == 6:
  59. return (time,
  60. b' '.join([values[x] for x in
  61. (b'MemTotal', b'MemFree', b'Buffers', b'Cached', b'SwapTotal', b'SwapFree')]) + b'\n')
  62. def _diskstats_is_relevant_line(self, linetokens):
  63. if len(linetokens) != 14:
  64. return False
  65. disk = linetokens[2]
  66. return self.diskstats_regex.match(disk)
  67. def _reduce_diskstats(self, time, data):
  68. relevant_tokens = filter(self._diskstats_is_relevant_line, map(lambda x: x.split(), data.split(b'\n')))
  69. diskdata = [0] * 3
  70. reduced = None
  71. for tokens in relevant_tokens:
  72. # rsect
  73. diskdata[0] += int(tokens[5])
  74. # wsect
  75. diskdata[1] += int(tokens[9])
  76. # use
  77. diskdata[2] += int(tokens[12])
  78. if self.diskstats_ltime:
  79. # We need to compute information about the time interval
  80. # since the last sampling and record the result as sample
  81. # for that point in the past.
  82. interval = time - self.diskstats_ltime
  83. if interval > 0:
  84. sums = [ a - b for a, b in zip(diskdata, self.diskstats_data) ]
  85. readTput = sums[0] / 2.0 * 100.0 / interval
  86. writeTput = sums[1] / 2.0 * 100.0 / interval
  87. util = float( sums[2] ) / 10 / interval
  88. util = max(0.0, min(1.0, util))
  89. reduced = (self.diskstats_ltime, (readTput, writeTput, util))
  90. self.diskstats_ltime = time
  91. self.diskstats_data = diskdata
  92. return reduced
  93. def _reduce_nop(self, time, data):
  94. return (time, data)
  95. def _reduce_stat(self, time, data):
  96. if not data:
  97. return None
  98. # CPU times {user, nice, system, idle, io_wait, irq, softirq} from first line
  99. tokens = data.split(b'\n', 1)[0].split()
  100. times = [ int(token) for token in tokens[1:] ]
  101. reduced = None
  102. if self.stat_ltimes:
  103. user = float((times[0] + times[1]) - (self.stat_ltimes[0] + self.stat_ltimes[1]))
  104. system = float((times[2] + times[5] + times[6]) - (self.stat_ltimes[2] + self.stat_ltimes[5] + self.stat_ltimes[6]))
  105. idle = float(times[3] - self.stat_ltimes[3])
  106. iowait = float(times[4] - self.stat_ltimes[4])
  107. aSum = max(user + system + idle + iowait, 1)
  108. reduced = (time, (user/aSum, system/aSum, iowait/aSum))
  109. self.stat_ltimes = times
  110. return reduced
  111. def sample(self, event, force):
  112. now = time.time()
  113. if (now - self.last_proc > self.min_seconds) or force:
  114. for filename, output, handler in self.proc_files:
  115. with open(os.path.join('/proc', filename), 'rb') as input:
  116. data = input.read()
  117. if handler:
  118. reduced = handler(now, data)
  119. else:
  120. reduced = (now, data)
  121. if reduced:
  122. if isinstance(reduced[1], bytes):
  123. # Use as it is.
  124. data = reduced[1]
  125. else:
  126. # Convert to a single line.
  127. data = (' '.join([str(x) for x in reduced[1]]) + '\n').encode('ascii')
  128. # Unbuffered raw write, less overhead and useful
  129. # in case that we end up with concurrent writes.
  130. os.write(output.fileno(),
  131. ('%.0f\n' % reduced[0]).encode('ascii') +
  132. data +
  133. b'\n')
  134. self.last_proc = now
  135. if isinstance(event, bb.event.MonitorDiskEvent) and \
  136. ((now - self.last_disk_monitor > self.min_seconds) or force):
  137. os.write(self.monitor_disk.fileno(),
  138. ('%.0f\n' % now).encode('ascii') +
  139. ''.join(['%s: %d\n' % (dev, sample.total_bytes - sample.free_bytes)
  140. for dev, sample in event.disk_usage.items()]).encode('ascii') +
  141. b'\n')
  142. self.last_disk_monitor = now