@@ -8,12 +8,170 @@ import subprocess
import signal
import time
import tempfile
+import math
+import sys
import libxml2
from rteval.Log import Log
from rteval.modules import rtevalModulePrototype
from rteval.systopology import cpuinfo, SysTopology
from rteval.cpulist_utils import expand_cpulist, collapse_cpulist
+class TLRunData:
+ ''' class to store instance data from a timerlat run '''
+ def __init__(self, coreid, datatype, priority, logfnc):
+ self.__id = coreid
+ self.__type = datatype
+ self.__priority = int(priority)
+ self.description = ''
+ self._log = logfnc
+ self.duration = ''
+ # histogram data, irqs, kernel threads and user threads per core
+ self.irqs = {}
+ self.thrs = {}
+ self.usrs = {}
+ self.__samples = {}
+ self.__numsamples = 0
+ self.min = 100000000
+ self.max = 0
+ self.__stddev = 0.0
+ self.__mean = 0.0
+ self.__mode = 0.0
+ self.__median = 0.0
+ self.__range = 0.0
+
+ def update_max(self, value):
+ """ highest bucket with a value """
+ if value > self.max:
+ self.max = value
+
+ def update_min(self, value):
+ """ lowest bucket with a value """
+ if value < self.min:
+ self.min = value
+
+ def bucket(self, index, val1, val2, val3):
+ """ Store results index=bucket number, val1=IRQ, val2=thr, val3=usr """
+ values = val1 + val2 + val3
+ self.__samples[index] = self.__samples.setdefault(index, 0) + values
+ self.irqs[index] = val1
+ self.thrs[index] = val2
+ self.usrs[index] = val3
+ if values:
+ self.update_max(index)
+ self.update_min(index)
+ self.__numsamples += values
+
+ def reduce(self):
+ """ Calculate statistics """
+ # Check to see if we have any samples. If there are 1 or 0, return
+ if self.__numsamples <= 1:
+ self._log(Log.DEBUG, f"skipping {self.__id} ({self.__numsamples} sampples)")
+ self.__mad = 0
+ self.__stddev = 0
+ return
+
+ self._log(Log.INFO, f"reducing {self.__id}")
+ total = 0 # total number of samples
+ total_us = 0
+ keys = list(self.__samples.keys())
+ keys.sort()
+
+ # if numsamples is odd, then + 1 gives us the actual mid
+ # if numsamples is even, we avg mid and mid + 1, so we actually
+ # want to know mid + 1 since we will combine it with mid and
+ # the lastkey if the last key is at the end of a previous bucket
+ mid = int(self.__numsamples / 2) + 1
+
+ # mean, mode and median
+ occurances = 0
+ lastkey = -1
+ for i in keys:
+ if mid > total and mid <= total + self.__samples[i]:
+ # Test if numsamples is even and if mid+1 is the next bucket
+ if self.__numsamples & 1 != 0 and mid == total+1:
+ self.__median = (lastkey + i) / 2
+ else:
+ self.__median = i
+ lastkey = i
+ total += self.__samples[i]
+ total_us += i * self.__samples[i]
+ if self.__samples[i] > occurances:
+ occurances = self.__samples[i]
+ self.__mode = i
+ self.__mean = float(total_us) / float(self.__numsamples)
+
+ # range
+ for i in keys:
+ if self.__samples[i]:
+ low = i
+ break
+ high = keys[-1]
+ while high and self.__samples.setdefault(high, 0) == 0:
+ high -= 1
+ self.__range = high - low
+
+ # Mean Absolute Deviation and Standard Deviation
+ madsum = 0
+ varsum = 0
+ for i in keys:
+ madsum += float(abs(float(i) - self.__mean) * self.__samples[i])
+ varsum += float(((float(i) - self.__mean) ** 2) * self.__samples[i])
+ self.__mad = madsum / self.__numsamples
+ self.__stddev = math.sqrt(varsum / (self.__numsamples - 1))
+
+
+ def MakeReport(self):
+ rep_n = libxml2.newNode(self.__type)
+
+ if self.__type == 'system':
+ rep_n.newProp('description', self.description)
+ else:
+ rep_n.newProp('id', str(self.__id))
+ rep_n.newProp('priority', str(self.__priority))
+
+ stat_n = rep_n.newChild(None, 'statistics', None)
+ stat_n.newTextChild(None, 'samples', str(self.__numsamples))
+
+ if self.__numsamples > 0:
+ n = stat_n.newTextChild(None, 'minimum', str(self.min))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'maximum', str(self.max))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'median', str(self.__median))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'mode', str(self.__mode))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'range', str(self.__range))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'mean', str(self.__mean))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'mean_absolute_deviation', str(self.__mad))
+ n.newProp('unit', 'us')
+
+ n = stat_n.newTextChild(None, 'standard_deviation', str(self.__stddev))
+ n.newProp('unit', 'us')
+
+ hist_n = rep_n.newChild(None, 'histogram', None)
+ hist_n.newProp('nbuckets', str(len(self.__samples)))
+
+ keys = list(self.__samples.keys())
+ keys.sort()
+ for k in keys:
+ if self.__samples[k] == 0:
+ # Don't report buckets without any samples
+ continue
+ b_n = hist_n.newChild(None, 'bucket', None)
+ b_n.newProp('index', str(k))
+ b_n.newProp('value', str(self.__samples[k]))
+
+ return rep_n
+
class Timerlat(rtevalModulePrototype):
""" measurement modules for rteval """
def __init__(self, config, logger=None):
@@ -23,6 +181,8 @@ class Timerlat(rtevalModulePrototype):
self.__numanodes = int(self.__cfg.setdefault('numanodes', 0))
self.__priority = int(self.__cfg.setdefault('priority', 95))
+ default_buckets = ModuleParameters()["buckets"]["default"]
+ self.__buckets = int(self.__cfg.setdefault('buckets', default_buckets))
self.__cpulist = self.__cfg.setdefault('cpulist', "")
self.__cpus = [str(c) for c in expand_cpulist(self.__cpulist)]
@@ -31,6 +191,20 @@ class Timerlat(rtevalModulePrototype):
self.__timerlat_out = None
self.__timerlat_err = None
self.__started = False
+
+ # Create a TLRunData object for each core we'll measure
+ info = cpuinfo()
+ self.__timerlatdata = {}
+ for core in self.__cpus:
+ self.__timerlatdata[core] = TLRunData(core, 'core', self.__priority,
+ logfnc=self._log)
+ self.__timerlatdata[core].description = info[core]['model name']
+
+ # Create a TLRunData object for the overall system
+ self.__timerlatdata['system'] = TLRunData('system', 'system',
+ self.__priority,
+ logfnc=self._log)
+ self.__timerlatdata['system'].description = (f"({self.__numcores} cores) ") + info['0']['model name']
self._log(Log.DEBUG, f"system using {self.__numcores} cpu cores")
@@ -43,6 +217,7 @@ class Timerlat(rtevalModulePrototype):
def _WorkloadPrepare(self):
self.__cmd = ['rtla', 'timerlat', 'hist', '-P', f'f:{int(self.__priority)}', '-u']
self.__cmd.append(f'-c{self.__cpulist}')
+ self.__cmd.append(f'-E{self.__buckets}')
self._log(Log.DEBUG, f'self.__cmd = {self.__cmd}')
self.__timerlat_out = tempfile.SpooledTemporaryFile(mode='w+b')
self.__timerlat_err = tempfile.SpooledTemporaryFile(mode='w+b')
@@ -77,16 +252,82 @@ class Timerlat(rtevalModulePrototype):
os.kill(self.__timerlat_process.pid, signal.SIGINT)
time.sleep(2)
- self._setFinished()
- self.__started = False
-
- def MakeReport(self):
+ # Parse histogram output
self.__timerlat_out.seek(0)
for line in self.__timerlat_out:
line = bytes.decode(line)
- print(line)
+
+ # Skip any blank lines
+ if not line:
+ continue
+
+ if line.startswith('#'):
+ if line.startswith('# Duration:'):
+ duration = line.split()[2]
+ duration += line.split()[3]
+ self.__timerlatdata['system'].duration = duration
+ continue
+ elif line.startswith('Index'):
+ #print(line)
+ continue
+ elif line.startswith('over:'):
+ #print(line)
+ continue
+ elif line.startswith('count:'):
+ #print(line)
+ continue
+ elif line.startswith('min:'):
+ #print(line)
+ continue
+ elif line.startswith('avg:'):
+ #print(line)
+ continue
+ elif line.startswith('max:'):
+ #print(line)
+ continue
+ else:
+ pass
+ #print(line)
+
+ vals = line.split()
+ if not vals:
+ # If we don't have any values, don't try parsing
+ continue
+ try:
+ # The index corresponds to the bucket number
+ index = int(vals[0])
+ except:
+ self._log(Log.DEBUG, f'timerlat: unexpected output: {line}')
+ continue
+
+ for i, core in enumerate(self.__cpus):
+ self.__timerlatdata[core].bucket(index, int(vals[i*3+1]),
+ int(vals[i*3+2]),
+ int(vals[i*3+3]))
+ self.__timerlatdata['system'].bucket(index, int(vals[i*3+1]),
+ int(vals[i*3+2]),
+ int(vals[i*3+3]))
+ # Generate statistics for each RunData object
+ for n in list(self.__timerlatdata.keys()):
+ self.__timerlatdata[n].reduce()
+
self.__timerlat_out.close()
+ self._setFinished()
+ self.__started = False
+
+ def MakeReport(self):
+ rep_n = libxml2.newNode('timerlat')
+ rep_n.newProp('command_line', ' '.join(self.__cmd))
+
+ rep_n.addChild(self.__timerlatdata['system'].MakeReport())
+ for thr in self.__cpus:
+ if str(thr) not in self.__timerlatdata:
+ continue
+ rep_n.addChild(self.__timerlatdata[str(thr)].MakeReport())
+
+ return rep_n
+
def ModuleInfo():
""" Required measurement module information """
@@ -97,7 +338,10 @@ def ModuleParameters():
""" default parameters """
return {"priority": {"descr": "Run rtla timerlat with this priority",
"default": 95,
- "metavar": "PRIO" }
+ "metavar": "PRIO" },
+ "buckets": {"descr": "Number of buckets",
+ "default": 3500,
+ "metavar": "NUM"}
}
def create(params, logger):
@@ -107,6 +351,10 @@ def create(params, logger):
if __name__ == '__main__':
from rteval.rtevalConfig import rtevalConfig
+ if os.getuid() != 0:
+ print("Must be root to run timerlat!")
+ sys.exit(1)
+
l = Log()
l.SetLogVerbosity(Log.INFO|Log.DEBUG|Log.ERR|Log.WARN)
@@ -128,4 +376,8 @@ if __name__ == '__main__':
tl._WorkloadTask()
time.sleep(RUNTIME)
tl._WorkloadCleanup()
- tl.MakeReport()
+ rep_n = tl.MakeReport()
+
+ xml = libxml2.newDoc('1.0')
+ xml.setRootElement(rep_n)
+ xml.saveFormatFileEnc('-', 'UTF-8', 1)
This adds calculating statitics such as samples (count), min, max, mean, median, mode, range, absolute and standard deviations to xml Currently it combines the kernel thread, IRQs and userspace threads into one statistic like it does with cyclictest, but in the future we can separate this out too. To see this functioning from git, do this, su -c 'python rteval/modules/measurement/timerlat.py ' Signed-off-by: John Kacur <jkacur@redhat.com> --- rteval/modules/measurement/timerlat.py | 266 ++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 7 deletions(-)