1 # SPDX-License-Identifier: Apache-2.0 2 # 3 # Copyright (C) 2015, ARM Limited and contributors. 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 # not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 # 17 18 import fileinput 19 import json 20 import os 21 import re 22 23 from collections import namedtuple 24 from wlgen import Workload 25 from devlib.utils.misc import ranges_to_list 26 27 import logging 28 29 _Phase = namedtuple('Phase', 'duration_s, period_ms, duty_cycle_pct') 30 class Phase(_Phase): 31 """ 32 Descriptor for an RT-App load phase 33 34 :param duration_s: the phase duration in [s]. 35 :type duration_s: int 36 37 :param period_ms: the phase period in [ms]. 38 :type period_ms: int 39 40 :param duty_cycle_pct: the generated load in [%]. 41 :type duty_cycle_pct: int 42 """ 43 pass 44 45 class RTA(Workload): 46 """ 47 Class for creating RT-App workloads 48 """ 49 50 def __init__(self, 51 target, 52 name, 53 calibration=None): 54 """ 55 :param target: Devlib target to run workload on. 56 :param name: Human-readable name for the workload. 57 :param calibration: CPU calibration specification. Can be obtained from 58 :meth:`calibrate`. 59 """ 60 61 # Setup logging 62 self._log = logging.getLogger('RTApp') 63 64 # rt-app calibration 65 self.pload = calibration 66 67 # TODO: Assume rt-app is pre-installed on target 68 # self.target.setup('rt-app') 69 70 super(RTA, self).__init__(target, name) 71 72 # rt-app executor 73 self.wtype = 'rtapp' 74 self.executor = 'rt-app' 75 76 # Default initialization 77 self.json = None 78 self.rta_profile = None 79 self.loadref = None 80 self.rta_cmd = None 81 self.rta_conf = None 82 self.test_label = None 83 84 # Setup RTA callbacks 85 self.setCallback('postrun', self.__postrun) 86 87 @staticmethod 88 def calibrate(target): 89 """ 90 Calibrate RT-App on each CPU in the system 91 92 :param target: Devlib target to run calibration on. 93 :returns: Dict mapping CPU numbers to RT-App calibration values. 94 """ 95 pload_regexp = re.compile(r'pLoad = ([0-9]+)ns') 96 pload = {} 97 98 # Setup logging 99 log = logging.getLogger('RTApp') 100 101 # Save previous governors 102 old_governors = {} 103 for domain in target.cpufreq.iter_domains(): 104 cpu = domain[0] 105 governor = target.cpufreq.get_governor(cpu) 106 tunables = target.cpufreq.get_governor_tunables(cpu) 107 old_governors[cpu] = governor, tunables 108 109 target.cpufreq.set_all_governors('performance') 110 111 for cpu in target.list_online_cpus(): 112 113 log.info('CPU%d calibration...', cpu) 114 115 max_rtprio = int(target.execute('ulimit -Hr').split('\r')[0]) 116 log.debug('Max RT prio: %d', max_rtprio) 117 if max_rtprio > 10: 118 max_rtprio = 10 119 120 rta = RTA(target, 'rta_calib') 121 rta.conf(kind='profile', 122 params = { 123 'task1': Periodic( 124 period_ms=100, 125 duty_cycle_pct=50, 126 duration_s=1, 127 sched={ 128 'policy': 'FIFO', 129 'prio' : max_rtprio 130 } 131 ).get() 132 }, 133 cpus=[cpu]) 134 rta.run(as_root=True) 135 136 for line in rta.getOutput().split('\n'): 137 pload_match = re.search(pload_regexp, line) 138 if pload_match is None: 139 continue 140 pload[cpu] = int(pload_match.group(1)) 141 log.debug('>>> cpu%d: %d', cpu, pload[cpu]) 142 143 # Restore previous governors 144 # Setting a governor & tunables for a cpu will set them for all cpus 145 # in the same clock domain, so only restoring them for one cpu 146 # per domain is enough to restore them all. 147 for cpu, (governor, tunables) in old_governors.iteritems(): 148 target.cpufreq.set_governor(cpu, governor) 149 target.cpufreq.set_governor_tunables(cpu, **tunables) 150 151 log.info('Target RT-App calibration:') 152 log.info("{" + ", ".join('"%r": %r' % (key, pload[key]) 153 for key in pload) + "}") 154 155 # Sanity check calibration values for big.LITTLE systems 156 if 'bl' in target.modules: 157 bcpu = target.bl.bigs_online[0] 158 lcpu = target.bl.littles_online[0] 159 if pload[bcpu] > pload[lcpu]: 160 log.warning('Calibration values reports big cores less ' 161 'capable than LITTLE cores') 162 raise RuntimeError('Calibration failed: try again or file a bug') 163 bigs_speedup = ((float(pload[lcpu]) / pload[bcpu]) - 1) * 100 164 log.info('big cores are ~%.0f%% more capable than LITTLE cores', 165 bigs_speedup) 166 167 return pload 168 169 def __postrun(self, params): 170 destdir = params['destdir'] 171 if destdir is None: 172 return 173 self._log.debug('Pulling logfiles to [%s]...', destdir) 174 for task in self.tasks.keys(): 175 logfile = self.target.path.join(self.run_dir, 176 '*{}*.log'.format(task)) 177 self.target.pull(logfile, destdir) 178 self._log.debug('Pulling JSON to [%s]...', destdir) 179 self.target.pull(self.target.path.join(self.run_dir, self.json), 180 destdir) 181 logfile = self.target.path.join(destdir, 'output.log') 182 self._log.debug('Saving output on [%s]...', logfile) 183 with open(logfile, 'w') as ofile: 184 for line in self.output['executor'].split('\n'): 185 ofile.write(line+'\n') 186 187 def getCalibrationConf(self): 188 # Select CPU for task calibration, which is the first little 189 # of big depending on the loadref tag 190 if self.pload is not None: 191 if self.loadref and self.loadref.upper() == 'LITTLE': 192 return max(self.pload.values()) 193 else: 194 return min(self.pload.values()) 195 else: 196 cpus = self.cpus or range(self.target.number_of_cpus) 197 198 target_cpu = cpus[-1] 199 if 'bl'in self.target.modules: 200 cluster = self.target.bl.bigs 201 candidates = sorted(set(self.target.bl.bigs).intersection(cpus)) 202 if candidates: 203 target_cpu = candidates[0] 204 205 return 'CPU{0:d}'.format(target_cpu) 206 207 def _confCustom(self): 208 209 rtapp_conf = self.params['custom'] 210 211 # Sanity check params being a valid file path 212 if not isinstance(rtapp_conf, str) or \ 213 not os.path.isfile(rtapp_conf): 214 self._log.debug('Checking for %s', rtapp_conf) 215 raise ValueError('value specified for \'params\' is not ' 216 'a valid rt-app JSON configuration file') 217 218 self._log.info('Loading custom configuration:') 219 self._log.info(' %s', rtapp_conf) 220 self.json = '{0:s}_{1:02d}.json'.format(self.name, self.exc_id) 221 ofile = open(self.json, 'w') 222 ifile = open(rtapp_conf, 'r') 223 224 calibration = self.getCalibrationConf() 225 # Calibration can either be a string like "CPU1" or an integer, if the 226 # former we need to quote it. 227 if type(calibration) != int: 228 calibration = '"{}"'.format(calibration) 229 230 replacements = { 231 '__DURATION__' : str(self.duration), 232 '__PVALUE__' : str(calibration), 233 '__LOGDIR__' : str(self.run_dir), 234 '__WORKDIR__' : '"'+self.target.working_directory+'"', 235 } 236 237 for line in ifile: 238 if '__DURATION__' in line and self.duration is None: 239 raise ValueError('Workload duration not specified') 240 for src, target in replacements.iteritems(): 241 line = line.replace(src, target) 242 ofile.write(line) 243 ifile.close() 244 ofile.close() 245 246 with open(self.json) as f: 247 conf = json.load(f) 248 for tid in conf['tasks']: 249 self.tasks[tid] = {'pid': -1} 250 251 return self.json 252 253 def _confProfile(self): 254 255 # Sanity check for task names 256 for task in self.params['profile'].keys(): 257 if len(task) > 15: 258 # rt-app uses pthread_setname_np(3) which limits the task name 259 # to 16 characters including the terminal '\0'. 260 msg = ('Task name "{}" too long, please configure your tasks ' 261 'with names shorter than 16 characters').format(task) 262 raise ValueError(msg) 263 264 # Task configuration 265 self.rta_profile = { 266 'tasks': {}, 267 'global': {} 268 } 269 270 # Initialize global configuration 271 global_conf = { 272 'default_policy': 'SCHED_OTHER', 273 'duration': -1, 274 'calibration': self.getCalibrationConf(), 275 'logdir': self.run_dir, 276 } 277 278 if self.duration is not None: 279 global_conf['duration'] = self.duration 280 self._log.warn('Limiting workload duration to %d [s]', 281 global_conf['duration']) 282 else: 283 self._log.info('Workload duration defined by longest task') 284 285 # Setup default scheduling class 286 if 'policy' in self.sched: 287 policy = self.sched['policy'].upper() 288 if policy not in ['OTHER', 'FIFO', 'RR', 'DEADLINE']: 289 raise ValueError('scheduling class {} not supported'\ 290 .format(policy)) 291 global_conf['default_policy'] = 'SCHED_' + self.sched['policy'] 292 293 self._log.info('Default policy: %s', global_conf['default_policy']) 294 295 # Setup global configuration 296 self.rta_profile['global'] = global_conf 297 298 # Setup tasks parameters 299 for tid in sorted(self.params['profile'].keys()): 300 task = self.params['profile'][tid] 301 302 # Initialize task configuration 303 task_conf = {} 304 305 if 'sched' not in task: 306 policy = 'DEFAULT' 307 else: 308 policy = task['sched']['policy'].upper() 309 if policy == 'DEFAULT': 310 task_conf['policy'] = global_conf['default_policy'] 311 sched_descr = 'sched: using default policy' 312 elif policy not in ['OTHER', 'FIFO', 'RR', 'DEADLINE']: 313 raise ValueError('scheduling class {} not supported'\ 314 .format(task['sclass'])) 315 else: 316 task_conf.update(task['sched']) 317 task_conf['policy'] = 'SCHED_' + policy 318 sched_descr = 'sched: {0:s}'.format(task['sched']) 319 320 # Initialize task phases 321 task_conf['phases'] = {} 322 323 self._log.info('------------------------') 324 self._log.info('task [%s], %s', tid, sched_descr) 325 326 if 'delay' in task.keys(): 327 if task['delay'] > 0: 328 task_conf['delay'] = int(task['delay'] * 1e6) 329 self._log.info(' | start delay: %.6f [s]', 330 task['delay']) 331 332 if 'loops' not in task.keys(): 333 task['loops'] = 1 334 task_conf['loop'] = task['loops'] 335 self._log.info(' | loops count: %d', task['loops']) 336 337 # Setup task affinity 338 if 'cpus' in task and task['cpus']: 339 self._log.info(' | CPUs affinity: %s', task['cpus']) 340 if isinstance(task['cpus'], str): 341 task_conf['cpus'] = ranges_to_list(task['cpus']) 342 elif isinstance(task['cpus'], list): 343 task_conf['cpus'] = task['cpus'] 344 else: 345 raise ValueError('cpus must be a list or string') 346 347 348 # Setup task configuration 349 self.rta_profile['tasks'][tid] = task_conf 350 351 # Getting task phase descriptor 352 pid=1 353 for phase in task['phases']: 354 355 # Convert time parameters to integer [us] units 356 duration = int(phase.duration_s * 1e6) 357 period = int(phase.period_ms * 1e3) 358 359 # A duty-cycle of 0[%] translates on a 'sleep' phase 360 if phase.duty_cycle_pct == 0: 361 362 self._log.info(' + phase_%06d: sleep %.6f [s]', 363 pid, duration/1e6) 364 365 task_phase = { 366 'loop': 1, 367 'sleep': duration, 368 } 369 370 # A duty-cycle of 100[%] translates on a 'run-only' phase 371 elif phase.duty_cycle_pct == 100: 372 373 self._log.info(' + phase_%06d: batch %.6f [s]', 374 pid, duration/1e6) 375 376 task_phase = { 377 'loop': 1, 378 'run': duration, 379 } 380 381 # A certain number of loops is requires to generate the 382 # proper load 383 else: 384 385 cloops = -1 386 if duration >= 0: 387 cloops = int(duration / period) 388 389 sleep_time = period * (100 - phase.duty_cycle_pct) / 100 390 running_time = period - sleep_time 391 392 self._log.info('+ phase_%06d: duration %.6f [s] (%d loops)', 393 pid, duration/1e6, cloops) 394 self._log.info('| period %6d [us], duty_cycle %3d %%', 395 period, phase.duty_cycle_pct) 396 self._log.info('| run_time %6d [us], sleep_time %6d [us]', 397 running_time, sleep_time) 398 399 task_phase = { 400 'loop': cloops, 401 'run': running_time, 402 'timer': {'ref': tid, 'period': period}, 403 } 404 405 self.rta_profile['tasks'][tid]['phases']\ 406 ['p'+str(pid).zfill(6)] = task_phase 407 408 pid+=1 409 410 # Append task name to the list of this workload tasks 411 self.tasks[tid] = {'pid': -1} 412 413 # Generate JSON configuration on local file 414 self.json = '{0:s}_{1:02d}.json'.format(self.name, self.exc_id) 415 with open(self.json, 'w') as outfile: 416 json.dump(self.rta_profile, outfile, 417 sort_keys=True, indent=4, separators=(',', ': ')) 418 419 return self.json 420 421 def conf(self, 422 kind, 423 params, 424 duration=None, 425 cpus=None, 426 sched=None, 427 run_dir=None, 428 exc_id=0, 429 loadref='big'): 430 """ 431 Configure a workload of a specified kind. 432 433 The rt-app based workload allows to define different classes of 434 workloads. The classes supported so far are detailed hereafter. 435 436 Custom workloads 437 When 'kind' is 'custom' the tasks generated by this workload are the 438 ones defined in a provided rt-app JSON configuration file. 439 In this case the 'params' parameter must be used to specify the 440 complete path of the rt-app JSON configuration file to use. 441 442 Profile based workloads 443 When ``kind`` is "profile", ``params`` is a dictionary mapping task 444 names to task specifications. The easiest way to create these task 445 specifications using :meth:`RTATask.get`. 446 447 For example, the following configures an RTA workload with a single 448 task, named 't1', using the default parameters for a Periodic RTATask: 449 450 :: 451 452 wl = RTA(...) 453 wl.conf(kind='profile', params={'t1': Periodic().get()}) 454 455 :param kind: Either 'custom' or 'profile' - see above. 456 :param params: RT-App parameters - see above. 457 :param duration: Maximum duration of the workload in seconds. Any 458 remaining tasks are killed by rt-app when this time has 459 elapsed. 460 :param cpus: CPUs to restrict this workload to, using ``taskset``. 461 :type cpus: list(int) 462 463 :param sched: Global RT-App scheduler configuration. Dict with fields: 464 465 policy 466 The default scheduler policy. Choose from 'OTHER', 'FIFO', 'RR', 467 and 'DEADLINE'. 468 469 :param run_dir: Target dir to store output and config files in. 470 471 .. TODO: document or remove loadref 472 """ 473 474 if not sched: 475 sched = {'policy' : 'OTHER'} 476 477 super(RTA, self).conf(kind, params, duration, 478 cpus, sched, run_dir, exc_id) 479 480 self.loadref = loadref 481 482 # Setup class-specific configuration 483 if kind == 'custom': 484 self._confCustom() 485 elif kind == 'profile': 486 self._confProfile() 487 488 # Move configuration file to target 489 self.target.push(self.json, self.run_dir) 490 491 self.rta_cmd = self.target.executables_directory + '/rt-app' 492 self.rta_conf = self.run_dir + '/' + self.json 493 self.command = '{0:s} {1:s} 2>&1'.format(self.rta_cmd, self.rta_conf) 494 495 # Set and return the test label 496 self.test_label = '{0:s}_{1:02d}'.format(self.name, self.exc_id) 497 return self.test_label 498 499 class RTATask(object): 500 """ 501 Base class for conveniently constructing params to :meth:`RTA.conf` 502 503 This class represents an RT-App task which may contain multiple phases. It 504 implements ``__add__`` so that using ``+`` on two tasks concatenates their 505 phases. For example ``Ramp() + Periodic()`` would yield an ``RTATask`` that 506 executes the default phases for ``Ramp`` followed by the default phases for 507 ``Periodic``. 508 """ 509 510 def __init__(self): 511 self._task = {} 512 513 def get(self): 514 """ 515 Return a dict that can be passed as an element of the ``params`` field 516 to :meth:`RTA.conf`. 517 """ 518 return self._task 519 520 def __add__(self, next_phases): 521 if next_phases._task.get('delay', 0): 522 # This won't work, because rt-app's "delay" field is per-task and 523 # not per-phase. We might be able to implement it by adding a 524 # "sleep" event here, but let's not bother unless such a need 525 # arises. 526 raise ValueError("Can't compose rt-app tasks " 527 "when the second has nonzero 'delay_s'") 528 529 self._task['phases'].extend(next_phases._task['phases']) 530 return self 531 532 533 class Ramp(RTATask): 534 """ 535 Configure a ramp load. 536 537 This class defines a task which load is a ramp with a configured number 538 of steps according to the input parameters. 539 540 :param start_pct: the initial load percentage. 541 :param end_pct: the final load percentage. 542 :param delta_pct: the load increase/decrease at each step, in percentage 543 points. 544 :param time_s: the duration in seconds of each load step. 545 :param period_ms: the period used to define the load in [ms]. 546 :param delay_s: the delay in seconds before ramp start. 547 :param loops: number of time to repeat the ramp, with the specified delay in 548 between. 549 550 :param sched: the scheduler configuration for this task. 551 :type sched: dict 552 553 :param cpus: the list of CPUs on which task can run. 554 :type cpus: list(int) 555 """ 556 557 def __init__(self, start_pct=0, end_pct=100, delta_pct=10, time_s=1, 558 period_ms=100, delay_s=0, loops=1, sched=None, cpus=None): 559 super(Ramp, self).__init__() 560 561 self._task['cpus'] = cpus 562 if not sched: 563 sched = {'policy' : 'DEFAULT'} 564 self._task['sched'] = sched 565 self._task['delay'] = delay_s 566 self._task['loops'] = loops 567 568 if start_pct not in range(0,101) or end_pct not in range(0,101): 569 raise ValueError('start_pct and end_pct must be in [0..100] range') 570 571 if start_pct >= end_pct: 572 if delta_pct > 0: 573 delta_pct = -delta_pct 574 delta_adj = -1 575 if start_pct <= end_pct: 576 if delta_pct < 0: 577 delta_pct = -delta_pct 578 delta_adj = +1 579 580 phases = [] 581 steps = range(start_pct, end_pct+delta_adj, delta_pct) 582 for load in steps: 583 if load == 0: 584 phase = Phase(time_s, 0, 0) 585 else: 586 phase = Phase(time_s, period_ms, load) 587 phases.append(phase) 588 589 self._task['phases'] = phases 590 591 class Step(Ramp): 592 """ 593 Configure a step load. 594 595 This class defines a task which load is a step with a configured initial and 596 final load. Using the ``loops`` param, this can be used to create a workload 597 that alternates between two load values. 598 599 :param start_pct: the initial load percentage. 600 :param end_pct: the final load percentage. 601 :param time_s: the duration in seconds of each load step. 602 :param period_ms: the period used to define the load in [ms]. 603 :param delay_s: the delay in seconds before ramp start. 604 :param loops: number of time to repeat the step, with the specified delay in 605 between. 606 607 :param sched: the scheduler configuration for this task. 608 :type sched: dict 609 610 :param cpus: the list of CPUs on which task can run. 611 :type cpus: list(int) 612 """ 613 614 def __init__(self, start_pct=0, end_pct=100, time_s=1, period_ms=100, 615 delay_s=0, loops=1, sched=None, cpus=None): 616 delta_pct = abs(end_pct - start_pct) 617 super(Step, self).__init__(start_pct, end_pct, delta_pct, time_s, 618 period_ms, delay_s, loops, sched, cpus) 619 620 class Pulse(RTATask): 621 """ 622 Configure a pulse load. 623 624 This class defines a task which load is a pulse with a configured 625 initial and final load. 626 627 The main difference with the 'step' class is that a pulse workload is 628 by definition a 'step down', i.e. the workload switch from an finial 629 load to a final one which is always lower than the initial one. 630 Moreover, a pulse load does not generate a sleep phase in case of 0[%] 631 load, i.e. the task ends as soon as the non null initial load has 632 completed. 633 634 :param start_pct: the initial load percentage. 635 :param end_pct: the final load percentage. Must be lower than ``start_pct`` 636 value. If end_pct is 0, the task end after the ``start_pct`` 637 period has completed. 638 :param time_s: the duration in seconds of each load step. 639 :param period_ms: the period used to define the load in [ms]. 640 :param delay_s: the delay in seconds before ramp start. 641 :param loops: number of time to repeat the pulse, with the specified delay 642 in between. 643 644 :param sched: the scheduler configuration for this task. 645 :type sched: dict 646 647 :param cpus: the list of CPUs on which task can run 648 :type cpus: list(int) 649 """ 650 651 def __init__(self, start_pct=100, end_pct=0, time_s=1, period_ms=100, 652 delay_s=0, loops=1, sched=None, cpus=None): 653 super(Pulse, self).__init__() 654 655 if end_pct >= start_pct: 656 raise ValueError('end_pct must be lower than start_pct') 657 658 self._task = {} 659 660 self._task['cpus'] = cpus 661 if not sched: 662 sched = {'policy' : 'DEFAULT'} 663 self._task['sched'] = sched 664 self._task['delay'] = delay_s 665 self._task['loops'] = loops 666 self._task['phases'] = {} 667 668 if end_pct not in range(0,101) or start_pct not in range(0,101): 669 raise ValueError('end_pct and start_pct must be in [0..100] range') 670 if end_pct >= start_pct: 671 raise ValueError('end_pct must be lower than start_pct') 672 673 phases = [] 674 for load in [start_pct, end_pct]: 675 if load == 0: 676 continue 677 phase = Phase(time_s, period_ms, load) 678 phases.append(phase) 679 680 self._task['phases'] = phases 681 682 683 class Periodic(Pulse): 684 """ 685 Configure a periodic load. This is the simplest type of RTA task. 686 687 This class defines a task which load is periodic with a configured 688 period and duty-cycle. 689 690 :param duty_cycle_pct: the load percentage. 691 :param duration_s: the total duration in seconds of the task. 692 :param period_ms: the period used to define the load in milliseconds. 693 :param delay_s: the delay in seconds before starting the periodic phase. 694 695 :param sched: the scheduler configuration for this task. 696 :type sched: dict 697 698 :param cpus: the list of CPUs on which task can run. 699 :type cpus: list(int) 700 """ 701 702 def __init__(self, duty_cycle_pct=50, duration_s=1, period_ms=100, 703 delay_s=0, sched=None, cpus=None): 704 super(Periodic, self).__init__(duty_cycle_pct, 0, duration_s, 705 period_ms, delay_s, 1, sched, cpus) 706