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 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
499class 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
533class 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
591class 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
620class 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
683class 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