diff --git a/src/python/m5/ext/pystats/abstract_stat.py b/src/python/m5/ext/pystats/abstract_stat.py index bae327fcf9..19932a1e29 100644 --- a/src/python/m5/ext/pystats/abstract_stat.py +++ b/src/python/m5/ext/pystats/abstract_stat.py @@ -99,3 +99,6 @@ class AbstractStat(SerializableStat): return self.children( lambda _name: re.match(pattern, _name), recursive=True ) + + def __getitem__(self, item: str): + return getattr(self, item) diff --git a/src/python/m5/ext/pystats/group.py b/src/python/m5/ext/pystats/group.py index 5b2e760b32..d37f39459a 100644 --- a/src/python/m5/ext/pystats/group.py +++ b/src/python/m5/ext/pystats/group.py @@ -25,6 +25,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from typing import ( + Any, Dict, List, Optional, @@ -62,3 +63,62 @@ class Group(AbstractStat): for key, value in kwargs.items(): setattr(self, key, value) + + +class SimObjectGroup(Group): + """A group of statistics encapulated within a SimObject.""" + + def __init__(self, **kwargs: Dict[str, Union[Group, Statistic]]): + super().__init__(type="SimObject", **kwargs) + + +class SimObjectVectorGroup(Group): + """A Vector of SimObject objects. I.e., that which would be constructed + from something like `system.cpu = [DerivO3CPU(), TimingSimpleCPU()]`. + """ + + def __init__(self, value: List[AbstractStat], **kwargs: Dict[str, Any]): + assert isinstance(value, list), "Value must be a list" + super().__init__(type="SimObjectVector", value=value, **kwargs) + + def __getitem__(self, index: Union[int, str, float]) -> AbstractStat: + if not isinstance(index, int): + raise KeyError( + f"Index {index} not found in int. Cannot index Array with " + "non-int" + ) + return self.value[index] + + def __iter__(self): + return iter(self.value) + + def __len__(self): + return len(self.value) + + def get_all_stats_of_name(self, name: str) -> List[AbstractStat]: + """ + Get all the stats in the vector of that name. Useful for performing + operations on all the stats of the same name in a vector. + """ + to_return = [] + for stat in self.value: + if hasattr(stat, name): + to_return.append(getattr(stat, name)) + + # If the name is in the format "sim.bla.whatever", we are looking for + # the "bla.whatever" stats in the "sim" group. + # This is messy, but it works. + name_split = name.split(".") + if len(name_split) == 1: + return to_return + + if name_split[0] not in self: + return to_return + + to_return.extend( + self[name_split[0]].get_all_stats_of_name(".".join(name_split[1:])) + ) + return to_return + + def __getitem__(self, item: int): + return self.value[item] diff --git a/src/python/m5/stats/gem5stats.py b/src/python/m5/stats/gem5stats.py index 98cd0b3df1..144c365001 100644 --- a/src/python/m5/stats/gem5stats.py +++ b/src/python/m5/stats/gem5stats.py @@ -41,6 +41,7 @@ from m5.ext.pystats.simstat import * from m5.ext.pystats.statistic import * from m5.ext.pystats.storagetype import * from m5.objects import * +from m5.params import SimObjectVector import _m5.stats @@ -83,33 +84,6 @@ class JsonOutputVistor: simstat.dump(fp=fp, **self.json_args) -def get_stats_group(group: _m5.stats.Group) -> Group: - """ - Translates a gem5 Group object into a Python stats Group object. A Python - statistic Group object is a dictionary of labeled Statistic objects. Any - gem5 object passed to this will have its ``getStats()`` and ``getStatGroups`` - function called, and all the stats translated (inclusive of the stats - further down the hierarchy). - - :param group: The gem5 _m5.stats.Group object to be translated to be a Python - stats Group object. Typically this will be a gem5 SimObject. - - :returns: The stats group object translated from the input gem5 object. - """ - - stats_dict = {} - - for stat in group.getStats(): - statistic = __get_statistic(stat) - if statistic is not None: - stats_dict[stat.name] = statistic - - for key in group.getStatGroups(): - stats_dict[key] = get_stats_group(group.getStatGroups()[key]) - - return Group(**stats_dict) - - def __get_statistic(statistic: _m5.stats.Info) -> Optional[Statistic]: """ Translates a _m5.stats.Info object into a Statistic object, to process @@ -302,8 +276,84 @@ def _prepare_stats(group: _m5.stats.Group): _prepare_stats(child) +def _process_simobject_object(simobject: SimObject) -> SimObjectGroup: + """ + Processes the stats of a SimObject, and returns a dictionary of the stats + for the SimObject with PyStats objects when appropriate. + + :param simobject: The SimObject to process the stats for. + + :returns: A dictionary of the PyStats stats for the SimObject. + """ + + assert isinstance( + simobject, SimObject + ), "simobject param must be a SimObject." + + stats = ( + { + "name": simobject.get_name(), + } + if simobject.get_name() + else {} + ) + + for stat in simobject.getStats(): + val = __get_statistic(stat) + if val: + stats[stat.name] = val + + for name, child in simobject._children.items(): + to_add = _process_simobject_stats(child) + if to_add: + stats[name] = to_add + + for name, child in sorted(simobject.getStatGroups().items()): + # Note: We are using the name of the group to determine if we have + # already processed the group as a child simobject or a statistic. + # This is to avoid SimObjectVector's being processed twice. It is far + # from an ideal solution, but it works for now. + if not any( + re.compile(f"{to_match}" + r"\d*").search(name) + for to_match in stats.keys() + ): + stats[name] = Group(**_process_simobject_stats(child)) + + return SimObjectGroup(**stats) + + +def _process_simobject_stats( + simobject: Union[ + SimObject, SimObjectVector, List[Union[SimObject, SimObjectVector]] + ] +) -> Union[List[Dict], Dict]: + """ + Processes the stats of a SimObject, SimObjectVector, or List of either, and + returns a dictionary of the PySqtats for the SimObject. + + :param simobject: The SimObject to process the stats for. + + :returns: A dictionary of the stats for the SimObject. + """ + + if isinstance(simobject, SimObject): + return _process_simobject_object(simobject) + + if isinstance(simobject, Union[List, SimObjectVector]): + stats_list = [] + for obj in simobject: + stats_list.append(_process_simobject_stats(obj)) + return SimObjectVectorGroup(value=stats_list) + + return {} + + def get_simstat( - root: Union[SimObject, List[SimObject]], prepare_stats: bool = True + root: Union[ + Union[SimObject, SimObjectVector], + List[Union[SimObject, SimObjectVector]], + ], + prepare_stats: bool = True, ) -> SimStat: """ This function will return the SimStat object for a simulation given a @@ -323,7 +373,7 @@ def get_simstat( :Returns: The SimStat Object of the current simulation. """ - stats_map = {} + creation_time = datetime.now() time_converstion = None # TODO https://gem5.atlassian.net/browse/GEM5-846 final_tick = Root.getInstance().resolveStat("finalTick").value @@ -334,29 +384,19 @@ def get_simstat( if prepare_stats: _m5.stats.processDumpQueue() - for r in root: - if isinstance(r, Root): - # The Root is a special case, we jump directly into adding its - # constituent Groups. - if prepare_stats: - _prepare_stats(r) - for key in r.getStatGroups(): - stats_map[key] = get_stats_group(r.getStatGroups()[key]) - elif isinstance(r, SimObject): - if prepare_stats: - _prepare_stats(r) - stats_map[r.get_name()] = get_stats_group(r) + if prepare_stats: + if isinstance(root, list): + for obj in root: + _prepare_stats(obj) else: - raise TypeError( - "Object (" + str(r) + ") passed is not a " - "SimObject. " + __name__ + " only processes " - "SimObjects, or a list of SimObjects." - ) + _prepare_stats(root) + + stats = _process_simobject_stats(root).__dict__ + stats["name"] = root.get_name() if root.get_name() else "root" return SimStat( creation_time=creation_time, - time_conversion=time_converstion, simulated_begin_time=simulated_begin_time, simulated_end_time=simulated_end_time, - **stats_map, + **stats, ) diff --git a/tests/gem5/stats/configs/pystat_sparse_dist_check.py b/tests/gem5/stats/configs/pystat_sparse_dist_check.py index 5603fbe467..57afbd7ffa 100644 --- a/tests/gem5/stats/configs/pystat_sparse_dist_check.py +++ b/tests/gem5/stats/configs/pystat_sparse_dist_check.py @@ -70,8 +70,9 @@ root = Root(full_system=False, system=stat_tester) m5.instantiate() m5.simulate() -simstats = get_simstat(root) -output = simstats.to_json()["system"] +simstats = get_simstat(stat_tester) +output = simstats.to_json() + value_dict = {} for sample in args.samples: @@ -90,7 +91,8 @@ for key in value_dict: } expected_output = { - "type": "Group", + "type": "SimObject", + "name": "system", "time_conversion": None, args.name: { "value": scaler_dict, @@ -99,6 +101,14 @@ expected_output = { }, } +# Remove the time related fields from the outputs if they exist. +# `creation_time` is not deterministic, and `simulated_begin_time` and +# simulated_end_time are not under test here. +for field in ["creation_time", "simulated_begin_time", "simulated_end_time"]: + for map in [output, expected_output]: + if field in map: + del map[field] + if output != expected_output: print("Output statistics do not match expected:", file=sys.stderr) print("", file=sys.stderr) diff --git a/tests/gem5/stats/configs/pystat_vector2d_check.py b/tests/gem5/stats/configs/pystat_vector2d_check.py index 5e07e23164..539a3cb6be 100644 --- a/tests/gem5/stats/configs/pystat_vector2d_check.py +++ b/tests/gem5/stats/configs/pystat_vector2d_check.py @@ -144,7 +144,8 @@ for x in range(args.num_vectors): } expected_output = { - "type": "Group", + "type": "SimObject", + "name": "system", "time_conversion": None, args.name: { "type": "Vector2d", @@ -159,7 +160,15 @@ m5.instantiate() m5.simulate() simstats = get_simstat(stat_tester) -output = simstats.to_json()["system"] +output = simstats.to_json() + +# Remove the time related fields from the outputs if they exist. +# `creation_time` is not deterministic, and `simulated_begin_time` and +# simulated_end_time are not under test here. +for field in ["creation_time", "simulated_begin_time", "simulated_end_time"]: + for map in [output, expected_output]: + if field in map: + del map[field] if output != expected_output: print("Output statistics do not match expected:", file=sys.stderr) diff --git a/tests/gem5/stats/configs/pystat_vector_check.py b/tests/gem5/stats/configs/pystat_vector_check.py index 65f847b28c..1f8b18a577 100644 --- a/tests/gem5/stats/configs/pystat_vector_check.py +++ b/tests/gem5/stats/configs/pystat_vector_check.py @@ -109,7 +109,8 @@ for i in range(len(args.value)): } expected_output = { - "type": "Group", + "type": "SimObject", + "name": "system", "time_conversion": None, args.name: { "value": value_dict, @@ -124,7 +125,15 @@ m5.instantiate() m5.simulate() simstats = get_simstat(stat_tester) -output = simstats.to_json()["system"] +output = simstats.to_json() + +# Remove the time related fields from the outputs if they exist. +# `creation_time` is not deterministic, and `simulated_begin_time` and +# simulated_end_time are not under test here. +for field in ["creation_time", "simulated_begin_time", "simulated_end_time"]: + for map in [output, expected_output]: + if field in map: + del map[field] if output != expected_output: print("Output statistics do not match expected:", file=sys.stderr) diff --git a/tests/gem5/stats/configs/pystats_simobjectvector_check.py b/tests/gem5/stats/configs/pystats_simobjectvector_check.py new file mode 100644 index 0000000000..1393cf98f3 --- /dev/null +++ b/tests/gem5/stats/configs/pystats_simobjectvector_check.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024 The Regents of the University of California +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer; +# redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution; +# neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""This script is used for checking that SimObject Vectorsare +correctly parsed through to the gem5 PyStats.""" + +import m5 +from m5.objects import ( + Root, + ScalarStatTester, + VectorStatTester, +) +from m5.stats.gem5stats import get_simstat + +root = Root(full_system=False) +root.stat_testers = [ + ScalarStatTester(name="placeholder", value=11), + ScalarStatTester( + name="placeholder", value=22, description="Index 2 desc." + ), + ScalarStatTester(name="placeholder", value=33), + VectorStatTester( + name="index_4", + values=[44, 55, 66], + description="A SimStat Vector within a SimObject Vector.", + ), +] + +m5.instantiate() +m5.simulate() + +simstat = get_simstat(root) + +# 'stat_testers' is a list of SimObjects +assert hasattr(simstat, "stat_testers"), "No stat_testers attribute found." +assert len(simstat.stat_testers) == 4, "stat_testers list is not of length 3." + +# Accessable by index. +simobject = simstat.stat_testers[0] + +# We can directly access the statistic we're interested in and its "str" +# representation should be the same as the value we set. In this case "11.0". +assert ( + str(simobject.placeholder) == "11.0" +), "placeholder value is not 11.0 ()." + +# They can also be accessed like so: +# "other_stat" is a SimObject with a single stat called "stat". +str( + simstat["stat_testers"][3]["index_4"][0] +) == "44.0", 'simstat[3]["index_4"][0] value is not 44.' + +# We can also access other stats like type and description. +assert simstat.stat_testers[1].placeholder.description == "Index 2 desc." +assert simstat.stat_testers[1].placeholder.type == "Scalar" diff --git a/tests/gem5/stats/configs/simstat_output_check.py b/tests/gem5/stats/configs/simstat_output_check.py index f794e63c48..e5379c4e39 100644 --- a/tests/gem5/stats/configs/simstat_output_check.py +++ b/tests/gem5/stats/configs/simstat_output_check.py @@ -67,7 +67,8 @@ stat_tester.name = args.name stat_tester.description = args.description stat_tester.value = args.value expected_output = { - "type": "Group", + "type": "SimObject", + "name": "system", "time_conversion": None, args.name: { "value": args.value, @@ -84,7 +85,15 @@ m5.instantiate() m5.simulate() simstats = get_simstat(stat_tester) -output = simstats.to_json()["system"] +output = simstats.to_json() + +# Remove the time related fields from the outputs if they exist. +# `creation_time` is not deterministic, and `simulated_begin_time` and +# simulated_end_time are not under test here. +for field in ["creation_time", "simulated_begin_time", "simulated_end_time"]: + for map in [output, expected_output]: + if field in map: + del map[field] if output != expected_output: print("Output statistics do not match expected:", file=sys.stderr) diff --git a/tests/gem5/stats/test_simstats_output.py b/tests/gem5/stats/test_simstats_output.py index 7d47245bdd..da0c9bdca9 100644 --- a/tests/gem5/stats/test_simstats_output.py +++ b/tests/gem5/stats/test_simstats_output.py @@ -235,3 +235,20 @@ gem5_verify_config( valid_isas=(constants.all_compiled_tag,), length=constants.quick_tag, ) + +gem5_verify_config( + name="simstat-simobjectvector-test", + fixtures=(), + verifiers=[], + config=joinpath( + config.base_dir, + "tests", + "gem5", + "stats", + "configs", + "pystats_simobjectvector_check.py", + ), + config_args=[], + valid_isas=(constants.all_compiled_tag,), + length=constants.quick_tag, +)