• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18import fileinput
19import json
20import os
21import re
22
23from collections import namedtuple
24from wlgen import Workload
25from devlib.utils.misc import ranges_to_list
26
27import logging
28
29_Phase = namedtuple('Phase', 'duration_s, period_ms, duty_cycle_pct')
30class 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
45class 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 _getFirstBiggest(self, cpus):
188        # Non big.LITTLE system:
189        if 'bl' not in self.target.modules:
190            # return the first CPU of the last cluster
191            platform = self.target.platform
192            cluster_last = list(set(platform.core_clusters))[-1]
193            cluster_cpus = [cpu_id
194                    for cpu_id, cluster_id in enumerate(platform.core_clusters)
195                                           if cluster_id == cluster_last]
196            # If CPUs have been specified': return the fist in the last cluster
197            if cpus:
198                for cpu_id in cpus:
199                    if cpu_id in cluster_cpus:
200                        return cpu_id
201            # Otherwise just return the first cpu of the last cluster
202            return cluster_cpus[0]
203
204        # big.LITTLE system:
205        for c in cpus:
206             if c not in self.target.bl.bigs:
207                continue
208             return c
209        # Only LITTLE CPUs, thus:
210        #  return the first possible cpu
211        return cpus[0]
212
213    def _getFirstBig(self, cpus=None):
214        # Non big.LITTLE system:
215        if 'bl' not in self.target.modules:
216            return self._getFirstBiggest(cpus)
217        if cpus:
218            for c in cpus:
219                if c not in self.target.bl.bigs:
220                    continue
221                return c
222        # Only LITTLE CPUs, thus:
223        #  return the first big core of the system
224        if self.target.big_core:
225            # Big.LITTLE system
226            return self.target.bl.bigs[0]
227        return 0
228
229    def _getFirstLittle(self, cpus=None):
230        # Non big.LITTLE system:
231        if 'bl' not in self.target.modules:
232            # return the first CPU of the first cluster
233            platform = self.target.platform
234            cluster_first = list(set(platform.core_clusters))[0]
235            cluster_cpus = [cpu_id
236                    for cpu_id, cluster_id in enumerate(platform.core_clusters)
237                                           if cluster_id == cluster_first]
238            # If CPUs have been specified': return the fist in the first cluster
239            if cpus:
240                for cpu_id in cpus:
241                    if cpu_id in cluster_cpus:
242                        return cpu_id
243            # Otherwise just return the first cpu of the first cluster
244            return cluster_cpus[0]
245
246        # Try to return one LITTLE CPUs among the specified ones
247        if cpus:
248            for c in cpus:
249                if c not in self.target.bl.littles:
250                    continue
251                return c
252        # Only big CPUs, thus:
253        #  return the first LITTLE core of the system
254        if self.target.little_core:
255            # Big.LITTLE system
256            return self.target.bl.littles[0]
257        return 0
258
259    def getTargetCpu(self, loadref):
260        # Select CPU for task calibration, which is the first little
261        # of big depending on the loadref tag
262        if self.pload is not None:
263            if loadref and loadref.upper() == 'LITTLE':
264                target_cpu = self._getFirstLittle()
265                self._log.debug('ref on LITTLE cpu: %d', target_cpu)
266            else:
267                target_cpu = self._getFirstBig()
268                self._log.debug('ref on big cpu: %d', target_cpu)
269            return target_cpu
270
271        # These options are selected only when RTApp has not been
272        # already calibrated
273        if self.cpus is None:
274            target_cpu = self._getFirstBig()
275            self._log.debug('ref on cpu: %d', target_cpu)
276        else:
277            target_cpu = self._getFirstBiggest(self.cpus)
278            self._log.debug('ref on (possible) biggest cpu: %d', target_cpu)
279        return target_cpu
280
281    def getCalibrationConf(self, target_cpu=0):
282        if self.pload is None:
283            return 'CPU{0:d}'.format(target_cpu)
284        return self.pload[target_cpu]
285
286    def _confCustom(self):
287
288        rtapp_conf = self.params['custom']
289
290        # Sanity check params being a valid file path
291        if not isinstance(rtapp_conf, str) or \
292           not os.path.isfile(rtapp_conf):
293            self._log.debug('Checking for %s', rtapp_conf)
294            raise ValueError('value specified for \'params\' is not '
295                             'a valid rt-app JSON configuration file')
296
297        if self.duration is None:
298            raise ValueError('Workload duration not specified')
299
300        target_cpu = self.getTargetCpu(self.loadref)
301        calibration = self.getCalibrationConf(target_cpu)
302
303        self._log.info('Loading custom configuration:')
304        self._log.info('   %s', rtapp_conf)
305        self.json = '{0:s}_{1:02d}.json'.format(self.name, self.exc_id)
306        ofile = open(self.json, 'w')
307        ifile = open(rtapp_conf, 'r')
308
309        # Calibration can either be a string like "CPU1" or an integer, if the
310        # former we need to quote it.
311        if type(calibration) != int:
312            calibration = '"{}"'.format(calibration)
313
314        replacements = {
315            '__DURATION__' : str(self.duration),
316            '__PVALUE__'   : str(calibration),
317            '__LOGDIR__'   : str(self.run_dir),
318            '__WORKDIR__'  : '"'+self.target.working_directory+'"',
319        }
320
321        for line in ifile:
322            for src, target in replacements.iteritems():
323                line = line.replace(src, target)
324            ofile.write(line)
325        ifile.close()
326        ofile.close()
327
328        with open(self.json) as f:
329            conf = json.load(f)
330        for tid in conf['tasks']:
331            self.tasks[tid] = {'pid': -1}
332
333        return self.json
334
335    def _confProfile(self):
336
337        # Sanity check for task names
338        for task in self.params['profile'].keys():
339            if len(task) > 15:
340                # rt-app uses pthread_setname_np(3) which limits the task name
341                # to 16 characters including the terminal '\0'.
342                msg = ('Task name "{}" too long, please configure your tasks '
343                       'with names shorter than 16 characters').format(task)
344                raise ValueError(msg)
345
346        # Task configuration
347        target_cpu = self.getTargetCpu(self.loadref)
348        self.rta_profile = {
349            'tasks': {},
350            'global': {}
351        }
352
353        # Initialize global configuration
354        global_conf = {
355                'default_policy': 'SCHED_OTHER',
356                'duration': -1,
357                'calibration': 'CPU'+str(target_cpu),
358                'logdir': self.run_dir,
359            }
360
361        # Setup calibration data
362        calibration = self.getCalibrationConf(target_cpu)
363        global_conf['calibration'] = calibration
364        if self.duration is not None:
365            global_conf['duration'] = self.duration
366            self._log.warn('Limiting workload duration to %d [s]',
367                           global_conf['duration'])
368        else:
369            self._log.info('Workload duration defined by longest task')
370
371        # Setup default scheduling class
372        if 'policy' in self.sched:
373            policy = self.sched['policy'].upper()
374            if policy not in ['OTHER', 'FIFO', 'RR', 'DEADLINE']:
375                raise ValueError('scheduling class {} not supported'\
376                        .format(policy))
377            global_conf['default_policy'] = 'SCHED_' + self.sched['policy']
378
379        self._log.info('Default policy: %s', global_conf['default_policy'])
380
381        # Setup global configuration
382        self.rta_profile['global'] = global_conf
383
384        # Setup tasks parameters
385        for tid in sorted(self.params['profile'].keys()):
386            task = self.params['profile'][tid]
387
388            # Initialize task configuration
389            task_conf = {}
390
391            if 'sched' not in task:
392                policy = 'DEFAULT'
393            else:
394                policy = task['sched']['policy'].upper()
395            if policy == 'DEFAULT':
396                task_conf['policy'] = global_conf['default_policy']
397                sched_descr = 'sched: using default policy'
398            elif policy not in ['OTHER', 'FIFO', 'RR', 'DEADLINE']:
399                raise ValueError('scheduling class {} not supported'\
400                        .format(task['sclass']))
401            else:
402                task_conf.update(task['sched'])
403                task_conf['policy'] = 'SCHED_' + policy
404                sched_descr = 'sched: {0:s}'.format(task['sched'])
405
406            # Initialize task phases
407            task_conf['phases'] = {}
408
409            self._log.info('------------------------')
410            self._log.info('task [%s], %s', tid, sched_descr)
411
412            if 'delay' in task.keys():
413                if task['delay'] > 0:
414                    task_conf['phases']['p000000'] = {}
415                    task_conf['phases']['p000000']['delay'] = int(task['delay'] * 1e6)
416                    self._log.info(' | start delay: %.6f [s]',
417                            task['delay'])
418
419            self._log.info(' | calibration CPU: %d', target_cpu)
420
421            if 'loops' not in task.keys():
422                task['loops'] = 1
423            task_conf['loop'] = task['loops']
424            self._log.info(' | loops count: %d', task['loops'])
425
426            # Setup task affinity
427            if 'cpus' in task and task['cpus']:
428                self._log.info(' | CPUs affinity: %s', task['cpus'])
429                if isinstance(task['cpus'], str):
430                    task_conf['cpus'] = ranges_to_list(task['cpus'])
431                elif isinstance(task['cpus'], list):
432                    task_conf['cpus'] = task['cpus']
433                else:
434                    raise ValueError('cpus must be a list or string')
435
436
437            # Setup task configuration
438            self.rta_profile['tasks'][tid] = task_conf
439
440            # Getting task phase descriptor
441            pid=1
442            for phase in task['phases']:
443
444                # Convert time parameters to integer [us] units
445                duration = int(phase.duration_s * 1e6)
446                period = int(phase.period_ms * 1e3)
447
448                # A duty-cycle of 0[%] translates on a 'sleep' phase
449                if phase.duty_cycle_pct == 0:
450
451                    self._log.info(' + phase_%06d: sleep %.6f [s]',
452                                   pid, duration/1e6)
453
454                    task_phase = {
455                        'loop': 1,
456                        'sleep': duration,
457                    }
458
459                # A duty-cycle of 100[%] translates on a 'run-only' phase
460                elif phase.duty_cycle_pct == 100:
461
462                    self._log.info(' + phase_%06d: batch %.6f [s]',
463                                   pid, duration/1e6)
464
465                    task_phase = {
466                        'loop': 1,
467                        'run': duration,
468                    }
469
470                # A certain number of loops is requires to generate the
471                # proper load
472                else:
473
474                    cloops = -1
475                    if duration >= 0:
476                        cloops = int(duration / period)
477
478                    sleep_time = period * (100 - phase.duty_cycle_pct) / 100
479                    running_time = period - sleep_time
480
481                    self._log.info('+ phase_%06d: duration %.6f [s] (%d loops)',
482                                   pid, duration/1e6, cloops)
483                    self._log.info('|  period   %6d [us], duty_cycle %3d %%',
484                                   period, phase.duty_cycle_pct)
485                    self._log.info('|  run_time %6d [us], sleep_time %6d [us]',
486                                   running_time, sleep_time)
487
488                    task_phase = {
489                        'loop': cloops,
490                        'run': running_time,
491                        'timer': {'ref': tid, 'period': period},
492                    }
493
494                self.rta_profile['tasks'][tid]['phases']\
495                    ['p'+str(pid).zfill(6)] = task_phase
496
497                pid+=1
498
499            # Append task name to the list of this workload tasks
500            self.tasks[tid] = {'pid': -1}
501
502        # Generate JSON configuration on local file
503        self.json = '{0:s}_{1:02d}.json'.format(self.name, self.exc_id)
504        with open(self.json, 'w') as outfile:
505            json.dump(self.rta_profile, outfile,
506                    sort_keys=True, indent=4, separators=(',', ': '))
507
508        return self.json
509
510    def conf(self,
511             kind,
512             params,
513             duration=None,
514             cpus=None,
515             sched=None,
516             run_dir=None,
517             exc_id=0,
518             loadref='big'):
519        """
520        Configure a workload of a specified kind.
521
522        The rt-app based workload allows to define different classes of
523        workloads. The classes supported so far are detailed hereafter.
524
525        Custom workloads
526          When 'kind' is 'custom' the tasks generated by this workload are the
527          ones defined in a provided rt-app JSON configuration file.
528          In this case the 'params' parameter must be used to specify the
529          complete path of the rt-app JSON configuration file to use.
530
531        Profile based workloads
532          When ``kind`` is "profile", ``params`` is a dictionary mapping task
533          names to task specifications. The easiest way to create these task
534          specifications using :meth:`RTATask.get`.
535
536          For example, the following configures an RTA workload with a single
537          task, named 't1', using the default parameters for a Periodic RTATask:
538
539          ::
540
541            wl = RTA(...)
542            wl.conf(kind='profile', params={'t1': Periodic().get()})
543
544        :param kind: Either 'custom' or 'profile' - see above.
545        :param params: RT-App parameters - see above.
546        :param duration: Maximum duration of the workload in seconds. Any
547                         remaining tasks are killed by rt-app when this time has
548                         elapsed.
549        :param cpus: CPUs to restrict this workload to, using ``taskset``.
550        :type cpus: list(int)
551
552        :param sched: Global RT-App scheduler configuration. Dict with fields:
553
554          policy
555            The default scheduler policy. Choose from 'OTHER', 'FIFO', 'RR',
556            and 'DEADLINE'.
557
558        :param run_dir: Target dir to store output and config files in.
559
560        .. TODO: document or remove loadref
561        """
562
563        if not sched:
564            sched = {'policy' : 'OTHER'}
565
566        super(RTA, self).conf(kind, params, duration,
567                cpus, sched, run_dir, exc_id)
568
569        self.loadref = loadref
570
571        # Setup class-specific configuration
572        if kind == 'custom':
573            self._confCustom()
574        elif kind == 'profile':
575            self._confProfile()
576
577        # Move configuration file to target
578        self.target.push(self.json, self.run_dir)
579
580        self.rta_cmd  = self.target.executables_directory + '/rt-app'
581        self.rta_conf = self.run_dir + '/' + self.json
582        self.command = '{0:s} {1:s} 2>&1'.format(self.rta_cmd, self.rta_conf)
583
584        # Set and return the test label
585        self.test_label = '{0:s}_{1:02d}'.format(self.name, self.exc_id)
586        return self.test_label
587
588class RTATask(object):
589    """
590    Base class for conveniently constructing params to :meth:`RTA.conf`
591
592    This class represents an RT-App task which may contain multiple phases. It
593    implements ``__add__`` so that using ``+`` on two tasks concatenates their
594    phases. For example ``Ramp() + Periodic()`` would yield an ``RTATask`` that
595    executes the default phases for ``Ramp`` followed by the default phases for
596    ``Periodic``.
597    """
598
599    def __init__(self):
600        self._task = {}
601
602    def get(self):
603        """
604        Return a dict that can be passed as an element of the ``params`` field
605        to :meth:`RTA.conf`.
606        """
607        return self._task
608
609    def __add__(self, next_phases):
610        self._task['phases'].extend(next_phases._task['phases'])
611        return self
612
613
614class Ramp(RTATask):
615    """
616    Configure a ramp load.
617
618    This class defines a task which load is a ramp with a configured number
619    of steps according to the input parameters.
620
621    :param start_pct: the initial load percentage.
622    :param end_pct: the final load percentage.
623    :param delta_pct: the load increase/decrease at each step, in percentage
624                      points.
625    :param time_s: the duration in seconds of each load step.
626    :param period_ms: the period used to define the load in [ms].
627    :param delay_s: the delay in seconds before ramp start.
628    :param loops: number of time to repeat the ramp, with the specified delay in
629                  between.
630
631    :param sched: the scheduler configuration for this task.
632    :type sched: dict
633
634    :param cpus: the list of CPUs on which task can run.
635    :type cpus: list(int)
636    """
637
638    def __init__(self, start_pct=0, end_pct=100, delta_pct=10, time_s=1,
639                 period_ms=100, delay_s=0, loops=1, sched=None, cpus=None):
640        super(Ramp, self).__init__()
641
642        self._task['cpus'] = cpus
643        if not sched:
644            sched = {'policy' : 'DEFAULT'}
645        self._task['sched'] = sched
646        self._task['delay'] = delay_s
647        self._task['loops'] = loops
648
649        if start_pct not in range(0,101) or end_pct not in range(0,101):
650            raise ValueError('start_pct and end_pct must be in [0..100] range')
651
652        if start_pct >= end_pct:
653            if delta_pct > 0:
654                delta_pct = -delta_pct
655            delta_adj = -1
656        if start_pct <= end_pct:
657            if delta_pct < 0:
658                delta_pct = -delta_pct
659            delta_adj = +1
660
661        phases = []
662        steps = range(start_pct, end_pct+delta_adj, delta_pct)
663        for load in steps:
664            if load == 0:
665                phase = Phase(time_s, 0, 0)
666            else:
667                phase = Phase(time_s, period_ms, load)
668            phases.append(phase)
669
670        self._task['phases'] = phases
671
672class Step(Ramp):
673    """
674    Configure a step load.
675
676    This class defines a task which load is a step with a configured initial and
677    final load. Using the ``loops`` param, this can be used to create a workload
678    that alternates between two load values.
679
680    :param start_pct: the initial load percentage.
681    :param end_pct: the final load percentage.
682    :param time_s: the duration in seconds of each load step.
683    :param period_ms: the period used to define the load in [ms].
684    :param delay_s: the delay in seconds before ramp start.
685    :param loops: number of time to repeat the step, with the specified delay in
686                  between.
687
688    :param sched: the scheduler configuration for this task.
689    :type sched: dict
690
691    :param cpus: the list of CPUs on which task can run.
692    :type cpus: list(int)
693    """
694
695    def __init__(self, start_pct=0, end_pct=100, time_s=1, period_ms=100,
696                 delay_s=0, loops=1, sched=None, cpus=None):
697        delta_pct = abs(end_pct - start_pct)
698        super(Step, self).__init__(start_pct, end_pct, delta_pct, time_s,
699                                   period_ms, delay_s, loops, sched, cpus)
700
701class Pulse(RTATask):
702    """
703    Configure a pulse load.
704
705    This class defines a task which load is a pulse with a configured
706    initial and final load.
707
708    The main difference with the 'step' class is that a pulse workload is
709    by definition a 'step down', i.e. the workload switch from an finial
710    load to a final one which is always lower than the initial one.
711    Moreover, a pulse load does not generate a sleep phase in case of 0[%]
712    load, i.e. the task ends as soon as the non null initial load has
713    completed.
714
715    :param start_pct: the initial load percentage.
716    :param end_pct: the final load percentage. Must be lower than ``start_pct``
717                    value. If end_pct is 0, the task end after the ``start_pct``
718                    period has completed.
719    :param time_s: the duration in seconds of each load step.
720    :param period_ms: the period used to define the load in [ms].
721    :param delay_s: the delay in seconds before ramp start.
722    :param loops: number of time to repeat the pulse, with the specified delay
723                  in between.
724
725    :param sched: the scheduler configuration for this task.
726    :type sched: dict
727
728    :param cpus: the list of CPUs on which task can run
729    :type cpus: list(int)
730    """
731
732    def __init__(self, start_pct=100, end_pct=0, time_s=1, period_ms=100,
733                 delay_s=0, loops=1, sched=None, cpus=None):
734        super(Pulse, self).__init__()
735
736        if end_pct >= start_pct:
737            raise ValueError('end_pct must be lower than start_pct')
738
739        self._task = {}
740
741        self._task['cpus'] = cpus
742        if not sched:
743            sched = {'policy' : 'DEFAULT'}
744        self._task['sched'] = sched
745        self._task['delay'] = delay_s
746        self._task['loops'] = loops
747        self._task['phases'] = {}
748
749        if end_pct not in range(0,101) or start_pct not in range(0,101):
750            raise ValueError('end_pct and start_pct must be in [0..100] range')
751        if end_pct >= start_pct:
752            raise ValueError('end_pct must be lower than start_pct')
753
754        phases = []
755        for load in [start_pct, end_pct]:
756            if load == 0:
757                continue
758            phase = Phase(time_s, period_ms, load)
759            phases.append(phase)
760
761        self._task['phases'] = phases
762
763
764class Periodic(Pulse):
765    """
766    Configure a periodic load. This is the simplest type of RTA task.
767
768    This class defines a task which load is periodic with a configured
769    period and duty-cycle.
770
771    :param duty_cycle_pct: the load percentage.
772    :param duration_s: the total duration in seconds of the task.
773    :param period_ms: the period used to define the load in milliseconds.
774    :param delay_s: the delay in seconds before starting the periodic phase.
775
776    :param sched: the scheduler configuration for this task.
777    :type sched: dict
778
779    :param cpus: the list of CPUs on which task can run.
780    :type cpus: list(int)
781    """
782
783    def __init__(self, duty_cycle_pct=50, duration_s=1, period_ms=100,
784                 delay_s=0, sched=None, cpus=None):
785        super(Periodic, self).__init__(duty_cycle_pct, 0, duration_s,
786                                       period_ms, delay_s, 1, sched, cpus)
787