stdlib: Improve PyStat support for SimObject Vectors

Change-Id: Iba0c93ffa5c4b18acf75af82965c63a8881df189
This commit is contained in:
Bobby R. Bruce
2024-05-02 04:55:58 -07:00
parent 178679cbfd
commit c0a1fa33fe
9 changed files with 292 additions and 57 deletions

View File

@@ -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)

View File

@@ -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]

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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)

View File

@@ -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,
)