Home | History | Annotate | Download | only in wlgen
      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