1# Copyright (c) 2014,2019 ARM Limited
2# All rights reserved.
3#
4# The license below extends only to copyright in the software and shall
5# not be construed as granting a license to any other intellectual
6# property including but not limited to intellectual property relating
7# to a hardware implementation of the functionality of the software
8# licensed hereunder.  You may use the software subject to the license
9# terms below provided that you ensure that this notice is replicated
10# unmodified and in its entirety in all distributions of the software,
11# modified or unmodified, in source code or in binary form.
12#
13# Redistribution and use in source and binary forms, with or without
14# modification, are permitted provided that the following conditions are
15# met: redistributions of source code must retain the above copyright
16# notice, this list of conditions and the following disclaimer;
17# redistributions in binary form must reproduce the above copyright
18# notice, this list of conditions and the following disclaimer in the
19# documentation and/or other materials provided with the distribution;
20# neither the name of the copyright holders nor the names of its
21# contributors may be used to endorse or promote products derived from
22# this software without specific prior written permission.
23#
24# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35#
36# Author: Andrew Bardsley
37
38# This script allows .ini and .json system config file generated from a
39# previous gem5 run to be read in and instantiated.
40#
41# This may be useful as a way of allowing variant run scripts (say,
42# with more complicated than usual checkpointing/stats dumping/
43# simulation control) to read pre-described systems from config scripts
44# with better system-description capabilities.  Splitting scripts
45# between system construction and run control may allow better
46# debugging.
47
48from __future__ import print_function
49from __future__ import absolute_import
50
51import argparse
52import ConfigParser
53import inspect
54import json
55import re
56import six
57import sys
58
59import m5
60import m5.ticks as ticks
61
62if six.PY3:
63    long = int
64
65sim_object_classes_by_name = {
66    cls.__name__: cls for cls in m5.objects.__dict__.values()
67    if inspect.isclass(cls) and issubclass(cls, m5.objects.SimObject) }
68
69# Add some parsing functions to Param classes to handle reading in .ini
70#   file elements.  This could be moved into src/python/m5/params.py if
71#   reading .ini files from Python proves to be useful
72
73def no_parser(cls, flags, param):
74    raise Exception('Can\'t parse string: %s for parameter'
75        ' class: %s' % (str(param), cls.__name__))
76
77def simple_parser(suffix='', cast=lambda i: i):
78    def body(cls, flags, param):
79        return cls(cast(param + suffix))
80    return body
81
82# def tick_parser(cast=m5.objects.Latency): # lambda i: i):
83def tick_parser(cast=lambda i: i):
84    def body(cls, flags, param):
85        old_param = param
86        ret = cls(cast(str(param) + 't'))
87        return ret
88    return body
89
90def addr_range_parser(cls, flags, param):
91    sys.stdout.flush()
92    _param = param.split(':')
93    (start, end) = _param[0:2]
94    if len(_param) == 2:
95        return m5.objects.AddrRange(start=long(start), end=long(end))
96    else:
97        assert len(_param) > 2
98        intlv_match = _param[2]
99        masks = [ long(m) for m in _param[3:] ]
100        return m5.objects.AddrRange(start=long(start), end=long(end),
101                                    masks=masks, intlvMatch=long(intlv_match))
102
103
104def memory_bandwidth_parser(cls, flags, param):
105    # The string will be in tick/byte
106    # Convert to byte/tick
107    value = 1.0 / float(param)
108    # Convert to byte/s
109    value = ticks.fromSeconds(value)
110    return cls('%fB/s' % value)
111
112# These parameters have trickier parsing from .ini files than might be
113#   expected
114param_parsers = {
115    'Bool': simple_parser(),
116    'ParamValue': no_parser,
117    'NumericParamValue': simple_parser(cast=long),
118    'TickParamValue': tick_parser(),
119    'Frequency': tick_parser(cast=m5.objects.Latency),
120    'Current': simple_parser(suffix='A'),
121    'Voltage': simple_parser(suffix='V'),
122    'Enum': simple_parser(),
123    'MemorySize': simple_parser(suffix='B'),
124    'MemorySize32': simple_parser(suffix='B'),
125    'AddrRange': addr_range_parser,
126    'String': simple_parser(),
127    'MemoryBandwidth': memory_bandwidth_parser,
128    'Time': simple_parser(),
129    'EthernetAddr': simple_parser()
130    }
131
132for name, parser in param_parsers.items():
133    setattr(m5.params.__dict__[name], 'parse_ini', classmethod(parser))
134
135class PortConnection(object):
136    """This class is similar to m5.params.PortRef but with just enough
137    information for ConfigManager"""
138
139    def __init__(self, object_name, port_name, index):
140        self.object_name = object_name
141        self.port_name = port_name
142        self.index = index
143
144    @classmethod
145    def from_string(cls, str):
146        m = re.match('(.*)\.([^.\[]+)(\[(\d+)\])?', str)
147        object_name, port_name, whole_index, index = m.groups()
148        if index is not None:
149            index = int(index)
150        else:
151            index = 0
152
153        return PortConnection(object_name, port_name, index)
154
155    def __str__(self):
156        return '%s.%s[%d]' % (self.object_name, self.port_name, self.index)
157
158    def __cmp__(self, right):
159        return cmp((self.object_name, self.port_name, self.index),
160            (right.object_name, right.port_name, right.index))
161
162def to_list(v):
163    """Convert any non list to a singleton list"""
164    if isinstance(v, list):
165        return v
166    else:
167        return [v]
168
169class ConfigManager(object):
170    """Manager for parsing a Root configuration from a config file"""
171    def __init__(self, config):
172        self.config = config
173        self.objects_by_name = {}
174        self.flags = config.get_flags()
175
176    def find_object(self, object_name):
177        """Find and configure (with just non-SimObject parameters)
178        a single object"""
179
180        if object_name == 'Null':
181            return NULL
182
183        if object_name in self.objects_by_name:
184            return self.objects_by_name[object_name]
185
186        object_type = self.config.get_param(object_name, 'type')
187
188        if object_type not in sim_object_classes_by_name:
189            raise Exception('No SimObject type %s is available to'
190                ' build: %s' % (object_type, object_name))
191
192        object_class = sim_object_classes_by_name[object_type]
193
194        parsed_params = {}
195
196        for param_name, param in object_class._params.items():
197            if issubclass(param.ptype, m5.params.ParamValue):
198                if isinstance(param, m5.params.VectorParamDesc):
199                    param_values = self.config.get_param_vector(object_name,
200                        param_name)
201
202                    param_value = [ param.ptype.parse_ini(self.flags, value)
203                        for value in param_values ]
204                else:
205                    param_value = param.ptype.parse_ini(
206                        self.flags, self.config.get_param(object_name,
207                        param_name))
208
209                parsed_params[param_name] = param_value
210
211        obj = object_class(**parsed_params)
212        self.objects_by_name[object_name] = obj
213
214        return obj
215
216    def fill_in_simobj_parameters(self, object_name, obj):
217        """Fill in all references to other SimObjects in an objects
218        parameters.  This relies on all referenced objects having been
219        created"""
220
221        if object_name == 'Null':
222            return NULL
223
224        for param_name, param in obj.__class__._params.items():
225            if issubclass(param.ptype, m5.objects.SimObject):
226                if isinstance(param, m5.params.VectorParamDesc):
227                    param_values = self.config.get_param_vector(object_name,
228                        param_name)
229
230                    setattr(obj, param_name,
231                            [ self.objects_by_name[name]
232                                  if name != 'Null' else m5.params.NULL
233                              for name in param_values ])
234                else:
235                    param_value = self.config.get_param(object_name,
236                        param_name)
237
238                    if param_value != 'Null':
239                        setattr(obj, param_name, self.objects_by_name[
240                            param_value])
241
242        return obj
243
244    def fill_in_children(self, object_name, obj):
245        """Fill in the children of this object.  This relies on all the
246        referenced objects having been created"""
247
248        children = self.config.get_object_children(object_name)
249
250        for child_name, child_paths in children:
251            param = obj.__class__._params.get(child_name, None)
252            if child_name == 'Null':
253                continue
254
255            if isinstance(child_paths, list):
256                child_list = [ self.objects_by_name[path]
257                    for path in child_paths ]
258            else:
259                child_list = self.objects_by_name[child_paths]
260
261            obj.add_child(child_name, child_list)
262
263            for path in to_list(child_paths):
264                self.fill_in_children(path, self.objects_by_name[path])
265
266        return obj
267
268    def parse_port_name(self, port):
269        """Parse the name of a port"""
270
271        m = re.match('(.*)\.([^.\[]+)(\[(\d+)\])?', port)
272        peer, peer_port, whole_index, index = m.groups()
273        if index is not None:
274            index = int(index)
275        else:
276            index = 0
277
278        return (peer, self.objects_by_name[peer], peer_port, index)
279
280    def gather_port_connections(self, object_name, obj):
281        """Gather all the port-to-port connections from the named object.
282        Returns a list of (PortConnection, PortConnection) with unordered
283        (wrt. master/slave) connection information"""
284
285        if object_name == 'Null':
286            return NULL
287
288        parsed_ports = []
289        for port_name, port in obj.__class__._ports.items():
290            # Assume that unnamed ports are unconnected
291            peers = self.config.get_port_peers(object_name, port_name)
292
293            for index, peer in zip(range(0, len(peers)), peers):
294                parsed_ports.append((
295                    PortConnection(object_name, port.name, index),
296                    PortConnection.from_string(peer)))
297
298        return parsed_ports
299
300    def bind_ports(self, connections):
301        """Bind all ports from the given connection list.  Note that the
302        connection list *must* list all connections with both (slave,master)
303        and (master,slave) orderings"""
304
305        # Markup a dict of how many connections are made to each port.
306        #   This will be used to check that the next-to-be-made connection
307        #   has a suitable port index
308        port_bind_indices = {}
309        for from_port, to_port in connections:
310            port_bind_indices[
311                (from_port.object_name, from_port.port_name)] = 0
312
313        def port_has_correct_index(port):
314            return port_bind_indices[
315                (port.object_name, port.port_name)] == port.index
316
317        def increment_port_index(port):
318            port_bind_indices[
319                (port.object_name, port.port_name)] += 1
320
321        # Step through the sorted connections.  Exactly one of
322        #   each (slave,master) and (master,slave) pairs will be
323        #   bindable because the connections are sorted.
324        # For example:        port_bind_indices
325        #   left      right   left right
326        #   a.b[0] -> d.f[1]  0    0 X
327        #   a.b[1] -> e.g     0    0    BIND!
328        #   e.g -> a.b[1]     1 X  0
329        #   d.f[0] -> f.h     0    0    BIND!
330        #   d.f[1] -> a.b[0]  1    0    BIND!
331        connections_to_make = []
332        for connection in sorted(connections):
333            from_port, to_port = connection
334
335            if (port_has_correct_index(from_port) and
336                port_has_correct_index(to_port)):
337
338                connections_to_make.append((from_port, to_port))
339
340                increment_port_index(from_port)
341                increment_port_index(to_port)
342
343        # Exactly half of the connections (ie. all of them, one per
344        #   direction) must now have been made
345        if (len(connections_to_make) * 2) != len(connections):
346            raise Exception('Port bindings can\'t be ordered')
347
348        # Actually do the binding
349        for from_port, to_port in connections_to_make:
350            from_object = self.objects_by_name[from_port.object_name]
351            to_object = self.objects_by_name[to_port.object_name]
352
353            setattr(from_object, from_port.port_name,
354                getattr(to_object, to_port.port_name))
355
356    def find_all_objects(self):
357        """Find and build all SimObjects from the config file and connect
358        their ports together as described.  Does not instantiate system"""
359
360        # Build SimObjects for all sections of the config file
361        #   populating not-SimObject-valued parameters
362        for object_name in self.config.get_all_object_names():
363            self.find_object(object_name)
364
365        # Add children to objects in the hierarchy from root
366        self.fill_in_children('root', self.find_object('root'))
367
368        # Now fill in SimObject-valued parameters in the knowledge that
369        #   this won't be interpreted as becoming the parent of objects
370        #   which are already in the root hierarchy
371        for name, obj in self.objects_by_name.items():
372            self.fill_in_simobj_parameters(name, obj)
373
374        # Gather a list of all port-to-port connections
375        connections = []
376        for name, obj in self.objects_by_name.items():
377            connections += self.gather_port_connections(name, obj)
378
379        # Find an acceptable order to bind those port connections and
380        #   bind them
381        self.bind_ports(connections)
382
383class ConfigFile(object):
384    def get_flags(self):
385        return set()
386
387    def load(self, config_file):
388        """Load the named config file"""
389        pass
390
391    def get_all_object_names(self):
392        """Get a list of all the SimObject paths in the configuration"""
393        pass
394
395    def get_param(self, object_name, param_name):
396        """Get a single param or SimObject reference from the configuration
397        as a string"""
398        pass
399
400    def get_param_vector(self, object_name, param_name):
401        """Get a vector param or vector of SimObject references from the
402        configuration as a list of strings"""
403        pass
404
405    def get_object_children(self, object_name):
406        """Get a list of (name, paths) for each child of this object.
407        paths is either a single string object path or a list of object
408        paths"""
409        pass
410
411    def get_port_peers(self, object_name, port_name):
412        """Get the list of connected port names (in the string form
413        object.port(\[index\])?) of the port object_name.port_name"""
414        pass
415
416class ConfigIniFile(ConfigFile):
417    def __init__(self):
418        self.parser = ConfigParser.ConfigParser()
419
420    def load(self, config_file):
421        self.parser.read(config_file)
422
423    def get_all_object_names(self):
424        return self.parser.sections()
425
426    def get_param(self, object_name, param_name):
427        return self.parser.get(object_name, param_name)
428
429    def get_param_vector(self, object_name, param_name):
430        return self.parser.get(object_name, param_name).split()
431
432    def get_object_children(self, object_name):
433        if self.parser.has_option(object_name, 'children'):
434            children = self.parser.get(object_name, 'children')
435            child_names = children.split()
436        else:
437            child_names = []
438
439        def make_path(child_name):
440            if object_name == 'root':
441                return child_name
442            else:
443                return '%s.%s' % (object_name, child_name)
444
445        return [ (name, make_path(name)) for name in child_names ]
446
447    def get_port_peers(self, object_name, port_name):
448        if self.parser.has_option(object_name, port_name):
449            peer_string = self.parser.get(object_name, port_name)
450            return peer_string.split()
451        else:
452            return []
453
454class ConfigJsonFile(ConfigFile):
455    def __init__(self):
456        pass
457
458    def is_sim_object(self, node):
459        return isinstance(node, dict) and 'path' in node
460
461    def find_all_objects(self, node):
462        if self.is_sim_object(node):
463            self.object_dicts[node['path']] = node
464
465        if isinstance(node, list):
466            for elem in node:
467                self.find_all_objects(elem)
468        elif isinstance(node, dict):
469            for elem in node.values():
470                self.find_all_objects(elem)
471
472    def load(self, config_file):
473        root = json.load(open(config_file, 'r'))
474        self.object_dicts = {}
475        self.find_all_objects(root)
476
477    def get_all_object_names(self):
478        return sorted(self.object_dicts.keys())
479
480    def parse_param_string(self, node):
481        if node is None:
482            return "Null"
483        elif self.is_sim_object(node):
484            return node['path']
485        else:
486            return str(node)
487
488    def get_param(self, object_name, param_name):
489        obj = self.object_dicts[object_name]
490
491        return self.parse_param_string(obj[param_name])
492
493    def get_param_vector(self, object_name, param_name):
494        obj = self.object_dicts[object_name]
495
496        return [ self.parse_param_string(p) for p in obj[param_name] ]
497
498    def get_object_children(self, object_name):
499        """It is difficult to tell which elements are children in the
500        JSON file as there is no explicit 'children' node.  Take any
501        element which is a full SimObject description or a list of
502        SimObject descriptions.  This will not work with a mixed list of
503        references and descriptions but that's a scenario that isn't
504        possible (very likely?) with gem5's binding/naming rules"""
505        obj = self.object_dicts[object_name]
506
507        children = []
508        for name, node in obj.items():
509            if self.is_sim_object(node):
510                children.append((name, node['path']))
511            elif isinstance(node, list) and node != [] and all([
512                self.is_sim_object(e) for e in node ]):
513                children.append((name, [ e['path'] for e in node ]))
514
515        return children
516
517    def get_port_peers(self, object_name, port_name):
518        """Get the 'peer' element of any node with 'peer' and 'role'
519        elements"""
520        obj = self.object_dicts[object_name]
521
522        peers = []
523        if port_name in obj and 'peer' in obj[port_name] and \
524            'role' in obj[port_name]:
525            peers = to_list(obj[port_name]['peer'])
526
527        return peers
528
529parser = argparse.ArgumentParser()
530
531parser.add_argument('config_file', metavar='config-file.ini',
532    help='.ini configuration file to load and run')
533parser.add_argument('--checkpoint-dir', type=str, default=None,
534                    help='A checkpoint to directory to restore when starting '
535                         'the simulation')
536
537args = parser.parse_args(sys.argv[1:])
538
539if args.config_file.endswith('.ini'):
540    config = ConfigIniFile()
541    config.load(args.config_file)
542else:
543    config = ConfigJsonFile()
544    config.load(args.config_file)
545
546ticks.fixGlobalFrequency()
547
548mgr = ConfigManager(config)
549
550mgr.find_all_objects()
551
552m5.instantiate(args.checkpoint_dir)
553
554exit_event = m5.simulate()
555print('Exiting @ tick %i because %s' % (m5.curTick(), exit_event.getCause()))
556