From c02c6cbceb332ca306b3789797f3924760f16ea8 Mon Sep 17 00:00:00 2001
From: Sebastian Goscik <sebastian.goscik@live.co.uk>
Date: Thu, 18 Aug 2016 14:03:17 +0100
Subject: [PATCH] Runtime Parameters: WIP - replace me

---
 wlauto/core/configuration/configuration.py | 181 +++++++++++++++++++++
 1 file changed, 181 insertions(+)

diff --git a/wlauto/core/configuration/configuration.py b/wlauto/core/configuration/configuration.py
index d71a95b0..1dd3f8b4 100644
--- a/wlauto/core/configuration/configuration.py
+++ b/wlauto/core/configuration/configuration.py
@@ -13,8 +13,10 @@
 # limitations under the License.
 
 import os
+import re
 from copy import copy
 from collections import OrderedDict, defaultdict
+from devlib import TargetError
 
 from wlauto.exceptions import ConfigError
 from wlauto.utils.misc import (get_article, merge_config_values)
@@ -303,10 +305,170 @@ class ConfigurationPoint(object):
     __str__ = __repr__
 
 
+class RuntimeParameter(object):
+
+    def __init__(self, name,
+                 kind=None,
+                 description=None,
+                 merge=False):
+
+        self.name = re.compile(name)
+        if kind is not None:
+            if not callable(kind):
+                raise ValueError('Kind must be callable.')
+            if kind in KIND_MAP:
+                kind = KIND_MAP[kind]
+        else:
+            kind = str
+        self.kind = kind
+        self.description = description
+        self.merge = merge
+
+    def validate_kind(self, value, name):
+        try:
+            value = self.kind(value)
+        except (ValueError, TypeError):
+            typename = get_type_name(self.kind)
+            msg = 'Bad value "{}" for {}; must be {} {}'
+            article = get_article(typename)
+            raise ConfigError(msg.format(value, name, article, typename))
+
+    def match(self, name):
+        if self.name.match(name):
+            return True
+        return False
+
+    def update_value(self, name, new_value, source, dest):
+        self.validate_kind(new_value, name)
+
+        if name in dest:
+            old_value, sources = dest[name]
+        else:
+            old_value = None
+            sources = {}
+        sources[source] = new_value
+
+        if self.merge:
+            new_value = merge_config_values(old_value, new_value)
+
+        dest[name] = (new_value, sources)
+
+
+class RuntimeParameterManager(object):
+
+    runtime_parameters = []
+
+    def __init__(self, target_manager):
+        self.state = {}
+        self.target_manager = target_manager
+
+    def get_initial_state(self):
+        """
+        Should be used to load the starting state from the device. This state
+        should be updated if any changes are made to the device, and they are successful.
+        """
+        pass
+
+    def match(self, name):
+        for rtp in self.runtime_parameters:
+            if rtp.match(name):
+                return True
+        return False
+
+    def update_value(self, name, value, source, dest):
+        for rtp in self.runtime_parameters:
+            if rtp.match(name):
+                rtp.update_value(name, value, source, dest)
+                break
+        else:
+            msg = 'Unknown runtime parameter "{}"'
+            raise ConfigError(msg.format(name))
+
+    def static_validation(self, params):
+        """
+        Validate values that do not require a active device connection.
+        This method should also pop all runtime parameters meant for this manager
+        from params, even if they are not beign statically validated.
+        """
+        pass
+
+    def dynamic_validation(self, params):
+        """
+        Validate values that require an active device connection
+        """
+        pass
+
+    def commit(self):
+        """
+        All values have been validated, this will now actually set values
+        """
+        pass
+
+################################
+### RuntimeParameterManagers ###
+################################
+
+class CpuFreqParameters(object):
+
+    runtime_parameters = {
+        "cores": RuntimeParameter("(.+)_cores"),
+        "min_frequency": RuntimeParameter("(.+)_min_frequency", kind=int),
+        "max_frequency": RuntimeParameter("(.+)_max_frequency", kind=int),
+        "frequency": RuntimeParameter("(.+)_frequency", kind=int),
+        "governor": RuntimeParameter("(.+)_governor"),
+        "governor_tunables": RuntimeParameter("(.+)_governor_tunables"),
+    }
+
+    def __init__(self, target):
+        super(CpuFreqParameters, self).__init__(target)
+        self.core_names = set(target.core_names)
+
+    def match(self, name):
+        for param in self.runtime_parameters.itervalues():
+            if param.match(name):
+                return True
+        return False
+
+    def update_value(self, name, value, source):
+        for param in self.runtime_parameters.iteritems():
+            core_name_match = param.name.match(name)
+            if not core_name_match:
+                continue
+
+            core_name = core_name_match.groups()[0]
+            if core_name not in self.core_names:
+                msg = '"{}" in {} is not a valid core name, must be in: {}'
+                raise ConfigError(msg.format(core_name, name, ", ".join(self.core_names)))
+
+            param.update_value(name, value, source)
+            break
+        else:
+            RuntimeError('"{}" does not belong to CpuFreqParameters'.format(name))
+
+    def _get_merged_value(self, core, param_name):
+        return self.runtime_parameters[param_name].merged_values["{}_{}".format(core, param_name)]
+
+    def _cross_validate(self, core):
+        min_freq = self._get_merged_value(core, "min_frequency")
+        max_frequency = self._get_merged_value(core, "max_frequency")
+        if max_frequency < min_freq:
+            msg = "{core}_max_frequency must be larger than {core}_min_frequency"
+            raise ConfigError(msg.format(core=core))
+        frequency = self._get_merged_value(core, "frequency")
+        if not min_freq < frequency < max_frequency:
+            msg = "{core}_frequency must be between {core}_min_frequency and {core}_max_frequency"
+            raise ConfigError(msg.format(core=core))
+        #TODO: more checks
+
+    def commit_to_device(self, target):
+        pass
+        # TODO: Write values to device is correct order ect
+
 #####################
 ### Configuration ###
 #####################
 
+
 # pylint: disable=too-many-nested-blocks, too-many-branches
 def merge_using_priority_specificity(generic_name, specific_name, plugin_cache):
     """
@@ -719,6 +881,22 @@ class JobSpec(Configuration):
 
         self.workload_parameters = workload_params
 
+    def merge_runtime_parameters(self, plugin_cache, target_manager):
+
+        # Order global runtime parameters
+        runtime_parameters = OrderedDict()
+        global_runtime_params = plugin_cache.get_plugin_config("runtime_parameters")
+        for source in plugin_cache.sources:
+            runtime_parameters[source] = global_runtime_params[source]
+
+        # Add runtime parameters from JobSpec
+        for source, values in self.to_merge['runtime_parameters'].iteritems():
+            runtime_parameters[source] = values
+
+        # Merge
+        self.runtime_parameters = target_manager.merge_runtime_parameters(runtime_parameters)
+
+
     def finalize(self):
         self.id = "-".join([source.config['id'] for source in self._sources[1:]])  # ignore first id, "global"
 
@@ -820,8 +998,11 @@ class JobGenerator(object):
 
                 # PHASE 2.2: Merge global, section and workload entry "workload_parameters"
                 job_spec.merge_workload_parameters(self.plugin_cache)
+                target_manager.static_runtime_parameter_validation(job_spec.runtime_parameters)
 
                 # TODO: PHASE 2.3: Validate device runtime/boot paramerers
+                job_spec.merge_runtime_parameters(self.plugin_cache, target_manager)
+                target_manager.validate_runtime_parameters(job_spec.runtime_parameters)
 
                 # PHASE 2.4: Disable globally disabled instrumentation
                 job_spec.set("instrumentation", self.disabled_instruments)