aboutsummaryrefslogtreecommitdiff
path: root/libs/utils/executor.py
blob: 7c7060b22fed9703c23816b4fd1155d0055d7a78 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2015, ARM Limited and contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from bart.common.Analyzer import Analyzer
import collections
from collections import namedtuple
import datetime
import gzip
import json
import os
import re
import time
import trappy
from devlib import TargetError

# Configure logging
import logging

# Add JSON parsing support
from conf import JsonConf

import wlgen

from devlib import TargetError

Experiment = namedtuple('Experiment', ['wload_name', 'wload',
                                       'conf', 'iteration', 'out_dir'])

class Executor():
    """
    Abstraction for running sets of experiments and gathering data from targets

    An executor can be configured to run a set of workloads (wloads) in each
    different target configuration of a specified set (confs). These wloads and
    confs can be specified by the "experiments_conf" input dictionary. Each
    (workload, conf, iteration) tuple is called an "experiment".

    After the workloads have been run, the Executor object's `experiments`
    attribute is a list of Experiment objects. The `out_dir` attribute of these
    objects can be used to find the results of the experiment. This output
    directory follows this format:

        results/<test_id>/<wltype>:<conf>:<wload>/<run_id>

    where:

        test_id
            Is the "tid" defined by the experiments_conf, or a timestamp based
            folder in case "tid" is not specified.
        wltype
            Is the class of workload executed, e.g. rtapp or sched_perf.
        conf
            Is the "tag" of one of the specified **confs**.
        wload
            Is the identifier of one of the specified **wloads**.
        run_id
            Is the progressive execution number from 1 up to the specified
            **iterations**.

    :param experiments_conf: Dict with experiment configuration. Keys are:

        **confs**
          Mandatory. Platform configurations to be tested. List of dicts,
          each with keys:

            tag
              String to identify this configuration. Required, may be empty.
            flags
              List of strings describing features required for this
              conf. Available flags are:

              "ftrace"
                  Enable collecting ftrace during the experiment.
              "freeze_userspace"
                  Use the cgroups freezer to freeze as many userspace tasks as
                  possible during the experiment execution, in order to reduce
                  system noise. Some tasks cannot be frozen, such as those
                  required to maintain a connection to LISA.

            sched_features
              Optional list of features to be written to
              /sys/kernel/debug/sched_features. Prepend "NO\_" to a feature to
              actively disable it. Requires ``CONFIG_SCHED_DEBUG`` in target
              kernel.
            cpufreq
              Parameters to configure cpufreq via Devlib's cpufreq
              module. Dictionary with fields:

              .. TODO link to devlib cpufreq module docs (which don't exist)

              governor
                cpufreq governor to set (for all CPUs) before execution. The
                previous governor is not restored when execution is finished.
              governor_tunables
                Dictionary of governor-specific tunables, expanded and passed as
                kwargs to the cpufreq module's ``set_governor_tunables`` method.
              freq
                Requires "governor" to be "userspace". Dictionary mapping CPU
                numbers to frequencies. Exact frequencies should be available on
                those CPUs. It is not necessary to provide a frequency for every
                CPU - the frequency for unspecified CPUs is not affected. Note
                that cpufreq will transparrently set the frequencies of any
                other CPUs sharing a clock domain.

            cgroups
              Optional cgroups configuration. To use this, ensure the 'cgroups'
              devlib module is enabled in your test_conf Contains fields:

              .. TODO reference test_conf
              .. TODO link to devlib cgroup module's docs (which don't exist)

              conf
                Dict specifying the cgroup controllers, cgroups, and cgroup
                parameters to setup. If a controller listed here is not
                enabled in the target kernel, a message is logged and the
                configuration is **ignored**. Of the form:

                ::

                  "<controller>" : {
                      "<group1>" : { "<group_param" : <value> }
                      "<group2>" : { "<group_param" : <value> }
                  }

                These cgroups can then be used in the "cgroup" field of workload
                specifications.

              default
                The default cgroup to run workloads in, if no "cgroup" is
                specified.

              For example, to create a cpuset cgroup named "/big" which
              restricts constituent tasks to CPUs 1 and 2:

              ::

                "cgroups" : {
                    "conf" : {
                        "cpuset" : {
                            "/big" : {"cpus" : "1-2"},
                        }
                    },
                    "default" : "/",
                }

          **wloads**
            .. TODO document wloads field.

            Mandatory. Workloads to run on each platform configuration

          **iterations**
            Number of iterations for each workload/conf combination. Default
            is 1.
    :type experiments_conf: dict
    """

    critical_tasks = {
        'linux': ['init', 'systemd', 'sh', 'ssh'],
        'android': [
            'sh', 'adbd',
            'usb', 'transport',
            # We don't actually need this task but on Google Pixel it apparently
            # cannot be frozen, so the cgroup state gets stuck in FREEZING if we
            # try to freeze it.
            'thermal-engine'
        ]
    }
    """
    Dictionary mapping OS name to list of task names that we can't afford to
    freeze when using freeeze_userspace.
    """

    def __init__(self, test_env, experiments_conf):
        # Initialize globals
        self._default_cgroup = None
        self._cgroup = None

        # Setup logging
        self._log = logging.getLogger('Executor')

        # Setup test configuration
        if isinstance(experiments_conf, dict):
            self._log.info('Loading custom (inline) test configuration')
            self._experiments_conf = experiments_conf
        elif isinstance(experiments_conf, str):
            self._log.info('Loading custom (file) test configuration')
            json_conf = JsonConf(experiments_conf)
            self._experiments_conf = json_conf.load()
        else:
            raise ValueError(
                'experiments_conf must be either a dictionary or a filepath')

        # Check for mandatory configurations
        if not self._experiments_conf.get('confs', None):
            raise ValueError('Configuration error: '
                             'missing "conf" definitions')
        if not self._experiments_conf.get('wloads', None):
            raise ValueError('Configuration error: '
                             'missing "wloads" definitions')

        self.te = test_env
        self.target = self.te.target

        self._iterations = self._experiments_conf.get('iterations', 1)
        # Compute total number of experiments
        self._exp_count = self._iterations \
                * len(self._experiments_conf['wloads']) \
                * len(self._experiments_conf['confs'])

        self._print_section('Experiments configuration')

        self._log.info('Configured to run:')

        self._log.info('   %3d target configurations:',
                       len(self._experiments_conf['confs']))
        target_confs = [conf['tag'] for conf in self._experiments_conf['confs']]
        target_confs = ', '.join(target_confs)
        self._log.info('      %s', target_confs)

        self._log.info('   %3d workloads (%d iterations each)',
                       len(self._experiments_conf['wloads']),
                       self._iterations)
        wload_confs = ', '.join(self._experiments_conf['wloads'])
        self._log.info('      %s', wload_confs)

        self._log.info('Total: %d experiments', self._exp_count)

        self._log.info('Results will be collected under:')
        self._log.info('      %s', self.te.res_dir)

        if any(wl['type'] == 'rt-app'
               for wl in self._experiments_conf['wloads'].values()):
            self._log.info('rt-app workloads found, installing tool on target')
            self.te.install_tools(['rt-app'])

    def run(self):
        self._print_section('Experiments execution')

        self.experiments = []

        # Run all the configured experiments
        exp_idx = 0
        for tc in self._experiments_conf['confs']:
            # TARGET: configuration
            if not self._target_configure(tc):
                continue
            for wl_idx in self._experiments_conf['wloads']:
                # TEST: configuration
                wload, test_dir = self._wload_init(tc, wl_idx)
                for itr_idx in range(1, self._iterations + 1):
                    exp = Experiment(
                        wload_name=wl_idx,
                        wload=wload,
                        conf=tc,
                        iteration=itr_idx,
                        out_dir=os.path.join(test_dir, str(itr_idx)))
                    self.experiments.append(exp)

                    # WORKLOAD: execution
                    self._wload_run(exp_idx, exp)
                    exp_idx += 1
            self._target_cleanup(tc)

        self._print_section('Experiments execution completed')
        self._log.info('Results available in:')
        self._log.info('      %s', self.te.res_dir)


################################################################################
# Target Configuration
################################################################################

    def _cgroups_init(self, tc):
        self._default_cgroup = None
        if 'cgroups' not in tc:
            return True
        if 'cgroups' not in self.target.modules:
            raise RuntimeError('CGroups module not available. Please ensure '
                               '"cgroups" is listed in your target/test modules')
        self._log.info('Initialize CGroups support...')
        errors = False
        for kind in tc['cgroups']['conf']:
            self._log.info('Setup [%s] CGroup controller...', kind)
            controller = self.target.cgroups.controller(kind)
            if not controller:
                self._log.warning('CGroups controller [%s] NOT available',
                                  kind)
                errors = True
        return not errors

    def _setup_kernel(self, tc):
        # Deploy kernel on the device
        self.te.install_kernel(tc, reboot=True)
        # Setup the rootfs for the experiments
        self._setup_rootfs(tc)

    def _setup_sched_features(self, tc):
        if 'sched_features' not in tc:
            self._log.debug('Scheduler features configuration not provided')
            return
        feats = tc['sched_features'].split(",")
        for feat in feats:
            self._log.info('Set scheduler feature: %s', feat)
            self.target.execute('echo {} > /sys/kernel/debug/sched_features'.format(feat),
                                as_root=True)

    def _setup_rootfs(self, tc):
        # Initialize CGroups if required
        self._cgroups_init(tc)
        # Setup target folder for experiments execution
        self.te.run_dir = os.path.join(
                self.target.working_directory, TGT_RUN_DIR)
        # Create run folder as tmpfs
        self._log.debug('Setup RT-App run folder [%s]...', self.te.run_dir)
        self.target.execute('[ -d {0} ] || mkdir {0}'\
                .format(self.te.run_dir))
        self.target.execute(
                'grep schedtest /proc/mounts || '\
                '  mount -t tmpfs -o size=1024m {} {}'\
                .format('schedtest', self.te.run_dir),
                as_root=True)
        # tmpfs mounts have an SELinux context with "tmpfs" as the type (while
        # other files we create have "shell_data_file"). That prevents non-root
        # users from creating files in tmpfs mounts. For now, just put SELinux
        # in permissive mode to get around that.
        try:
            # First, save the old SELinux mode
            self._old_selinux_mode = self.target.execute('getenforce')
            self._log.warning('Setting target SELinux in permissive mode')
            self.target.execute('setenforce 0', as_root=True)
        except TargetError:
            # Probably the target doesn't have SELinux, or there are no
            # contexts set up. No problem.
            self._log.warning("Couldn't set SELinux in permissive mode. "
                                "This is probably fine.")
            self._old_selinux_mode = None

    def _setup_cpufreq(self, tc):
        if 'cpufreq' not in tc:
            self._log.warning('cpufreq governor not specified, '
                              'using currently configured governor')
            return

        cpufreq = tc['cpufreq']
        self._log.info('Configuring all CPUs to use [%s] cpufreq governor',
                       cpufreq['governor'])

        self.target.cpufreq.set_all_governors(cpufreq['governor'])

        if 'freqs' in cpufreq:
            if cpufreq['governor'] != 'userspace':
                raise ValueError('Must use userspace governor to set CPU freqs')
            self._log.info(r'%14s - CPU frequencies: %s',
                    'CPUFreq', str(cpufreq['freqs']))
            for cpu, freq in cpufreq['freqs'].iteritems():
                self.target.cpufreq.set_frequency(cpu, freq)

        if 'params' in cpufreq:
            self._log.info('governor params: %s', str(cpufreq['params']))
            for cpu in self.target.list_online_cpus():
                self.target.cpufreq.set_governor_tunables(
                        cpu,
                        cpufreq['governor'],
                        **cpufreq['params'])

    def _setup_cgroups(self, tc):
        if 'cgroups' not in tc:
            return True
        # Setup default CGroup to run tasks into
        if 'default' in tc['cgroups']:
            self._default_cgroup = tc['cgroups']['default']
        # Configure each required controller
        if 'conf' not in tc['cgroups']:
            return True
        errors = False
        for kind in tc['cgroups']['conf']:
            controller = self.target.cgroups.controller(kind)
            if not controller:
                self._log.warning('Configuration error: '
                                  '[%s] contoller NOT supported',
                                  kind)
                errors = True
                continue
            self._setup_controller(tc, controller)
        return not errors

    def _setup_controller(self, tc, controller):
        kind = controller.kind
        # Configure each required groups for that controller
        errors = False
        for name in tc['cgroups']['conf'][controller.kind]:
            if name[0] != '/':
                raise ValueError('Wrong CGroup name [{}]. '
                                 'CGroups names must start by "/".'
                                 .format(name))
            group = controller.cgroup(name)
            if not group:
                self._log.warning('Configuration error: '
                                  '[%s/%s] cgroup NOT available',
                                  kind, name)
                errors = True
                continue
            self._setup_group(tc, group)
        return not errors

    def _setup_group(self, tc, group):
        kind = group.controller.kind
        name = group.name
        # Configure each required attribute
        group.set(**tc['cgroups']['conf'][kind][name])

    def _setup_files(self, tc):
        if 'files' not in tc:
            self._log.debug('\'files\' Configuration block not provided')
            return True
        for name, value in tc['files'].iteritems():
            check = False
            if name.startswith('!/'):
                check = True
                name = name[1:]
            self._log.info('File Write(check=%s): \'%s\' -> \'%s\'',
                         check, value, name)
            try:
                self.target.write_value(name, value, True)
            except TargetError:
                self._log.info('File Write Failed: \'%s\' -> \'%s\'',
                         value, name)
                if check:
                    raise
        return False

    def _target_configure(self, tc):
        self._print_header(
                'configuring target for [{}] experiments'\
                .format(tc['tag']))
        self._setup_kernel(tc)
        self._setup_sched_features(tc)
        self._setup_cpufreq(tc)
        self._setup_files(tc)
        return self._setup_cgroups(tc)

    def _target_conf_flag(self, tc, flag):
        if 'flags' not in tc:
            has_flag = False
        else:
            has_flag = flag in tc['flags']
        self._log.debug('Check if target configuration [%s] has flag [%s]: %s',
                        tc['tag'], flag, has_flag)
        return has_flag

    def _target_cleanup(self, tc):
        if self._old_selinux_mode is not None:
            self._log.info('Restoring target SELinux mode: %s',
                           self._old_selinux_mode)
            self.target.execute('setenforce ' + self._old_selinux_mode,
                                as_root=True)

################################################################################
# Workload Setup and Execution
################################################################################

    def _wload_cpus(self, wl_idx, wlspec):
        if not 'cpus' in wlspec['conf']:
            return None
        cpus = wlspec['conf']['cpus']

        if type(cpus) == list:
            return cpus
        if type(cpus) == int:
            return [cpus]

        # SMP target (or not bL module loaded)
        if not hasattr(self.target, 'bl'):
            if 'first' in cpus:
                return [ self.target.list_online_cpus()[0] ]
            if 'last' in cpus:
                return [ self.target.list_online_cpus()[-1] ]
            return self.target.list_online_cpus()

        # big.LITTLE target
        if cpus.startswith('littles'):
            if 'first' in cpus:
                return [ self.target.bl.littles_online[0] ]
            if 'last' in cpus:
                return [ self.target.bl.littles_online[-1] ]
            return self.target.bl.littles_online
        if cpus.startswith('bigs'):
            if 'first' in cpus:
                return [ self.target.bl.bigs_online[0] ]
            if 'last' in cpus:
                return [ self.target.bl.bigs_online[-1] ]
            return self.target.bl.bigs_online
        raise ValueError('unsupported [{}] "cpus" value for [{}] '
                         'workload specification'
                         .format(cpus, wl_idx))

    def _wload_task_idxs(self, wl_idx, tasks):
        if type(tasks) == int:
            return range(tasks)
        if tasks == 'cpus':
            return range(len(self.target.core_names))
        if tasks == 'little':
            return range(len([t
                for t in self.target.core_names
                if t == self.target.little_core]))
        if tasks == 'big':
            return range(len([t
                for t in self.target.core_names
                if t == self.target.big_core]))
        raise ValueError('unsupported "tasks" value for [{}] RT-App '
                         'workload specification'
                         .format(wl_idx))

    def _wload_rtapp(self, wl_idx, wlspec, cpus):
        conf = wlspec['conf']
        self._log.debug('Configuring [%s] rt-app...', conf['class'])

        # Setup a default "empty" task name prefix
        if 'prefix' not in conf:
            conf['prefix'] = 'task_'

        # Setup a default loadref CPU
        loadref = None
        if 'loadref' in wlspec:
            loadref = wlspec['loadref']

        if conf['class'] == 'profile':
            params = {}
            # Load each task specification
            for task_name, task in conf['params'].items():
                if task['kind'] not in wlgen.__dict__:
                    self._log.error('RTA task of kind [%s] not supported',
                                    task['kind'])
                    raise ValueError('unsupported "kind" value for task [{}] '
                                     'in RT-App workload specification'
                                     .format(task))
                task_ctor = getattr(wlgen, task['kind'])
                num_tasks = task.get('tasks', 1)
                task_idxs = self._wload_task_idxs(wl_idx, num_tasks)
                for idx in task_idxs:
                    idx_name = "_{}".format(idx) if len(task_idxs) > 1 else ""
                    task_name_idx = conf['prefix'] + task_name + idx_name
                    params[task_name_idx] = task_ctor(**task['params']).get()

            rtapp = wlgen.RTA(self.target,
                        wl_idx, calibration = self.te.calibration())
            rtapp.conf(kind='profile', params=params, loadref=loadref,
                       cpus=cpus, run_dir=self.te.run_dir,
                       duration=conf.get('duration'))
            return rtapp

        if conf['class'] == 'periodic':
            task_idxs = self._wload_task_idxs(wl_idx, conf['tasks'])
            params = {}
            for idx in task_idxs:
                task = conf['prefix'] + str(idx)
                params[task] = wlgen.Periodic(**conf['params']).get()
            rtapp = wlgen.RTA(self.target,
                        wl_idx, calibration = self.te.calibration())
            rtapp.conf(kind='profile', params=params, loadref=loadref,
                       cpus=cpus, run_dir=self.te.run_dir,
                       duration=conf.get('duration'))
            return rtapp

        if conf['class'] == 'custom':
            rtapp = wlgen.RTA(self.target,
                        wl_idx, calibration = self.te.calib)
            rtapp.conf(kind='custom',
                    params=conf['json'],
                    duration=conf['duration'],
                    loadref=loadref,
                    cpus=cpus, run_dir=self.te.run_dir)
            return rtapp

        raise ValueError('unsupported \'class\' value for [{}] '
                         'RT-App workload specification'
                         .format(wl_idx))

    def _wload_perf_bench(self, wl_idx, wlspec, cpus):
        conf = wlspec['conf']
        self._log.debug('Configuring perf_message...')

        if conf['class'] == 'messaging':
            perf_bench = wlgen.PerfMessaging(self.target, wl_idx)
            perf_bench.conf(**conf['params'])
            return perf_bench

        if conf['class'] == 'pipe':
            perf_bench = wlgen.PerfPipe(self.target, wl_idx)
            perf_bench.conf(**conf['params'])
            return perf_bench

        raise ValueError('unsupported "class" value for [{}] '
                         'perf bench workload specification'
                         .format(wl_idx))

    def _wload_conf(self, wl_idx, wlspec):

        # CPUS: setup execution on CPUs if required by configuration
        cpus = self._wload_cpus(wl_idx, wlspec)

        # CGroup: setup CGroups if requried by configuration
        self._cgroup = self._default_cgroup
        if 'cgroup' in wlspec:
            if 'cgroups' not in self.target.modules:
                raise RuntimeError('Target not supporting CGroups or CGroups '
                                   'not configured for the current test configuration')
            self._cgroup = wlspec['cgroup']

        if wlspec['type'] == 'rt-app':
            return self._wload_rtapp(wl_idx, wlspec, cpus)
        if wlspec['type'] == 'perf_bench':
            return self._wload_perf_bench(wl_idx, wlspec, cpus)


        raise ValueError('unsupported "type" value for [{}] '
                         'workload specification'
                         .format(wl_idx))

    def _wload_init(self, tc, wl_idx):
        tc_idx = tc['tag']

        # Configure the test workload
        wlspec = self._experiments_conf['wloads'][wl_idx]
        wload = self._wload_conf(wl_idx, wlspec)

        # Keep track of platform configuration
        test_dir = '{}/{}:{}:{}'\
            .format(self.te.res_dir, wload.wtype, tc_idx, wl_idx)
        os.makedirs(test_dir)
        self.te.platform_dump(test_dir)

        # Keep track of kernel configuration and version
        config = self.target.config
        with gzip.open(os.path.join(test_dir, 'kernel.config'), 'wb') as fh:
            fh.write(config.text)
        output = self.target.execute('{} uname -a'\
                .format(self.target.busybox))
        with open(os.path.join(test_dir, 'kernel.version'), 'w') as fh:
            fh.write(output)

        return wload, test_dir

    def _wload_run(self, exp_idx, experiment):
        tc = experiment.conf
        wload = experiment.wload
        tc_idx = tc['tag']

        self._print_title('Experiment {}/{}, [{}:{}] {}/{}'\
                .format(exp_idx, self._exp_count,
                        tc_idx, experiment.wload_name,
                        experiment.iteration, self._iterations))

        # Setup local results folder
        self._log.debug('out_dir set to [%s]', experiment.out_dir)
        os.system('mkdir -p ' + experiment.out_dir)

        # Freeze all userspace tasks that we don't need for running tests
        need_thaw = False
        if self._target_conf_flag(tc, 'freeze_userspace'):
            need_thaw = self._freeze_userspace()

        # FTRACE: start (if a configuration has been provided)
        if self.te.ftrace and self._target_conf_flag(tc, 'ftrace'):
            self._log.warning('FTrace events collection enabled')
            self.te.ftrace.start()

        # ENERGY: start sampling
        if self.te.emeter:
            self.te.emeter.reset()

        # WORKLOAD: Run the configured workload
        wload.run(out_dir=experiment.out_dir, cgroup=self._cgroup)

        # ENERGY: collect measurements
        if self.te.emeter:
            self.te.emeter.report(experiment.out_dir)

        # FTRACE: stop and collect measurements
        if self.te.ftrace and self._target_conf_flag(tc, 'ftrace'):
            self.te.ftrace.stop()

            trace_file = experiment.out_dir + '/trace.dat'
            self.te.ftrace.get_trace(trace_file)
            self._log.info('Collected FTrace binary trace:')
            self._log.info('   %s',
                           trace_file.replace(self.te.res_dir, '<res_dir>'))

            stats_file = experiment.out_dir + '/trace_stat.json'
            self.te.ftrace.get_stats(stats_file)
            self._log.info('Collected FTrace function profiling:')
            self._log.info('   %s',
                           stats_file.replace(self.te.res_dir, '<res_dir>'))

        # Unfreeze the tasks we froze
        if need_thaw:
            self._thaw_userspace()

        self._print_footer()

    def _freeze_userspace(self):
        if 'cgroups' not in self.target.modules:
            raise RuntimeError(
                'Failed to freeze userspace. Ensure "cgroups" module is listed '
                'among modules in target/test configuration')
        controllers = [s.name for s in self.target.cgroups.list_subsystems()]
        if 'freezer' not in controllers:
            self._log.warning('No freezer cgroup controller on target. '
                              'Not freezing userspace')
            return False

        exclude = self.critical_tasks[self.te.target.os]
        self._log.info('Freezing all tasks except: %s', ','.join(exclude))
        self.te.target.cgroups.freeze(exclude)
        return True


    def _thaw_userspace(self):
        self._log.info('Un-freezing userspace tasks')
        self.te.target.cgroups.freeze(thaw=True)

################################################################################
# Utility Functions
################################################################################

    def _print_section(self, message):
        self._log.info('')
        self._log.info(FMT_SECTION)
        self._log.info(message)
        self._log.info(FMT_SECTION)

    def _print_header(self, message):
        self._log.info('')
        self._log.info(FMT_HEADER)
        self._log.info(message)

    def _print_title(self, message):
        self._log.info(FMT_TITLE)
        self._log.info(message)

    def _print_footer(self, message=None):
        if message:
            self._log.info(message)
        self._log.info(FMT_FOOTER)


################################################################################
# Globals
################################################################################

# Regular expression for comments
JSON_COMMENTS_RE = re.compile(
    '(^)?[^\S\n]*/(?:\*(.*?)\*/[^\S\n]*|/[^\n]*)($)?',
    re.DOTALL | re.MULTILINE
)

# Target specific paths
TGT_RUN_DIR = 'run_dir'

# Logging formatters
FMT_SECTION = r'{:#<80}'.format('')
FMT_HEADER  = r'{:=<80}'.format('')
FMT_TITLE   = r'{:~<80}'.format('')
FMT_FOOTER  = r'{:-<80}'.format('')

# vim :set tabstop=4 shiftwidth=4 expandtab