Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Introduce permanent result storage and support performance results comparisons between runs #3227

Merged
merged 69 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
5e90f69
Index job reports
ekouts May 14, 2024
36a3efd
Refine report indexing
vkarak May 22, 2024
4438503
Performance comparison of past results
vkarak May 27, 2024
71c777d
Refactor reporting code
vkarak Jun 18, 2024
e73e7d6
Support for selecting sessions explicitly
vkarak Jun 28, 2024
e6afa06
Fix unit tests
vkarak Jun 28, 2024
7a30788
Fix coding style issues
vkarak Jun 28, 2024
fe36ed5
Fix `tabulate` requirement for Python 3.6
vkarak Jul 1, 2024
f56d1ed
Add an API around results storage and merge `analytics` with `reporting`
vkarak Jul 1, 2024
ed4f728
Add missing file
vkarak Jul 1, 2024
f68a092
Enhance printer to print tabulated data
vkarak Jul 1, 2024
77aac04
Move comparison spec code to a separate utility package
vkarak Jul 1, 2024
0b52501
Improve access to default storage backend
vkarak Jul 1, 2024
5a5b381
Add configuration option for specifying the perf. report spec
vkarak Jul 1, 2024
864917c
Remove unused import
vkarak Jul 1, 2024
3137f15
Python 3.6 compatible default value for `perf_report_spec`
vkarak Jul 1, 2024
15dd76c
Fix coding style issues
vkarak Jul 2, 2024
2ca850c
Improve formatting of performance report
vkarak Jul 2, 2024
9d6d1bb
Improve error handling and fix handling of `perf_report_spec`
vkarak Jul 8, 2024
0266495
Fix handling of `const` optional argument in `ArgumentParser`
vkarak Jul 8, 2024
9b68376
Synchronize storing to the DB and saving the report file
vkarak Jul 10, 2024
1108fcf
Use session ID as UUID
vkarak Jul 10, 2024
3df63f2
Remove unused import + fix `setup.cfg`
vkarak Jul 10, 2024
aa75c37
Group all actions in a required mutually exclusive option group
vkarak Jul 10, 2024
8df09ce
Add `--detect-host-topology` to actions
vkarak Jul 10, 2024
81a9513
Fix unit tests
vkarak Jul 10, 2024
5f6e12d
Fetch test cases from session exclusively
vkarak Jul 11, 2024
1350456
Allow grouping testcasesby runid and session_id`
vkarak Jul 11, 2024
92f9136
Use proper UUIDs as session IDs
vkarak Jul 11, 2024
d000d63
New command-line options for interacting with stored sessions
vkarak Jul 12, 2024
50294e7
Make report generation optional + add new configuration section for t…
vkarak Jul 15, 2024
edf5d54
Add Sqlite DB schema version check
vkarak Jul 16, 2024
addcde0
Support `w` for denoting weeks in period specs
vkarak Jul 16, 2024
efb3319
Improve error handling
vkarak Jul 16, 2024
5b7412a
Do not generate reports and DB entries if report is empty
vkarak Jul 16, 2024
240d72b
Address remaining MR comments
vkarak Jul 17, 2024
b0548fc
Group by also system, partition and environment
vkarak Jul 17, 2024
a85d1bf
Use the last successful retry as the reference test case for the perf…
vkarak Jul 17, 2024
e2b4c51
Move perflog testing to a different file
vkarak Jul 18, 2024
7d9b9da
More report tests to a `test_reporting.py`
vkarak Jul 18, 2024
b8d8e76
Add unit tests for reporting backend
vkarak Jul 18, 2024
88f6863
Add CLI unit tests for storage options
vkarak Aug 7, 2024
d05e0f4
Use alternative method for session deletion with Sqlite < 3.35
vkarak Aug 7, 2024
60b3791
Fix unit tests for Python 3.6
vkarak Aug 7, 2024
4fd81ca
Update manpage
vkarak Aug 8, 2024
61c29fc
Adapt tutorial
vkarak Aug 9, 2024
8e2cb61
Fix comment typos
vkarak Aug 9, 2024
71f8e5a
Treat correctly invalid completion times in `--list-stored-testcases`
vkarak Aug 9, 2024
7fa684b
Do not prefix UUIDs with `^`
vkarak Aug 9, 2024
d2e0844
Accept a time period argument in `--list-stored-sessions`
vkarak Aug 12, 2024
d08528f
Fix broken `nodelist` check for PBS-based backends
vkarak Aug 13, 2024
ddb8ad7
Allow setting the database file from an environment variable
vkarak Aug 13, 2024
b1206ad
Add CLI and config option to control the table format
vkarak Aug 14, 2024
db5b4be
More compact pretty table output
vkarak Aug 14, 2024
91c87c2
Add profiler calls to SQlite storage backend
vkarak Aug 14, 2024
34969e5
Optimize decoding of JSON blobs
vkarak Aug 14, 2024
5aa3da5
Create DB index for testcases table
vkarak Aug 22, 2024
dd37f86
Support filtering by testcase query options
vkarak Aug 22, 2024
e0f4320
Add unit tests for test filtering in testcase query options
vkarak Aug 23, 2024
159894d
Do not print "ReFrame setup" header with `--performance-compare` option
vkarak Aug 23, 2024
c4f6fc1
Do not store session if dry run
vkarak Aug 23, 2024
f2d3223
Update docs and tutorial
vkarak Aug 23, 2024
e2c7cb5
Fix listing formatting in tutorial
vkarak Aug 23, 2024
d85690c
Support for using always unqualified hostnames in local scheduler
vkarak Aug 23, 2024
4088f2b
Make perf. reference selectable as a column
vkarak Aug 23, 2024
73af49f
Apply suggestions from code review
vkarak Aug 28, 2024
d2612e6
Print info line about results DB file
vkarak Aug 28, 2024
75763c9
Fix coding style
vkarak Aug 28, 2024
2572d1f
Address remaining PR comments
vkarak Aug 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions reframe/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,17 @@ def what(exc_type, exc_value, tb):
reason += f': {exc_value}'

return reason


class reraise_as:
def __init__(self, new_exc, exceptions=(Exception,), message=''):
self.__new_exc = new_exc
self.__exceptions = exceptions
self.__message = message

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if isinstance(exc_val, self.__exceptions):
raise self.__new_exc(self.__message) from exc_val
11 changes: 6 additions & 5 deletions reframe/core/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ def _create_file_handler(site_config, config_prefix):

def _create_filelog_handler(site_config, config_prefix):
basedir = os.path.abspath(os.path.join(
site_config.get(f'systems/0/prefix'),
site_config.get('systems/0/prefix'),
osext.expandvars(site_config.get(f'{config_prefix}/basedir'))
))
prefix = osext.expandvars(site_config.get(f'{config_prefix}/prefix'))
Expand Down Expand Up @@ -581,7 +581,7 @@ def _create_httpjson_handler(site_config, config_prefix):

def _record_to_json(record, extras, ignore_keys):
def _can_send(key):
return not key.startswith('_') and not key in ignore_keys
return not key.startswith('_') and key not in ignore_keys

def _sanitize(s):
return re.sub(r'\W', '_', s)
Expand Down Expand Up @@ -860,9 +860,10 @@ def log_performance(self, level, task, msg=None, multiline=False):
if self.check is None or not self.check.is_performance_check():
return

self.extra['check_partition'] = task.testcase.partition.name
self.extra['check_environ'] = task.testcase.environ.name
self.extra['check_result'] = 'pass' if task.succeeded else 'fail'
_, part, env = task.testcase
self.extra['check_partition'] = part.name
self.extra['check_environ'] = env.name
self.extra['check_result'] = task.result
fail_reason = what(*task.exc_info) if not task.succeeded else None
self.extra['check_fail_reason'] = fail_reason
if msg is None:
Expand Down
16 changes: 8 additions & 8 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@


class _NoRuntime(ContainerPlatform):
'''Proxy container runtime for storing container platform info early enough.
'''Proxy container runtime for storing container platform info early
enough.

This will be replaced by the framework with a concrete implementation
based on the current partition info.
Expand Down Expand Up @@ -847,8 +848,8 @@ def pipeline_hooks(cls):
#: .. deprecated:: 4.0.0
#: Please use :attr:`env_vars` instead.
variables = deprecate(variable(alias=env_vars),
f"the use of 'variables' is deprecated; "
f"please use 'env_vars' instead")
"the use of 'variables' is deprecated; "
"please use 'env_vars' instead")

#: Time limit for this test.
#:
Expand Down Expand Up @@ -1517,8 +1518,7 @@ def _job_exitcode(self):
@loggable_as('job_nodelist')
@property
def _job_nodelist(self):
if self.job:
return self.job.nodelist
return self.job.nodelist if self.job else []

def info(self):
'''Provide live information for this test.
Expand Down Expand Up @@ -1705,7 +1705,7 @@ def _setup_build_job(self, **job_opts):
)

def _setup_run_job(self, **job_opts):
self._job = self._create_job(f'run', self.local, **job_opts)
self._job = self._create_job('run', self.local, **job_opts)

def _setup_container_platform(self):
try:
Expand Down Expand Up @@ -2217,7 +2217,7 @@ def check_performance(self):

if perf_patterns is not None and self.perf_variables:
raise ReframeSyntaxError(
f"you cannot mix 'perf_patterns' and 'perf_variables' syntax"
"you cannot mix 'perf_patterns' and 'perf_variables' syntax"
)

# Convert `perf_patterns` to `perf_variables`
Expand Down Expand Up @@ -2365,7 +2365,7 @@ def cleanup(self, remove_files=False):
aliased = os.path.samefile(self._stagedir, self._outputdir)
if aliased:
self.logger.debug(
f'outputdir and stagedir are the same; copying skipped'
'outputdir and stagedir are the same; copying skipped'
)
else:
self._copy_to_outputdir()
Expand Down
13 changes: 9 additions & 4 deletions reframe/core/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ class RuntimeContext:
.. versionadded:: 2.13
'''

def __init__(self, site_config):
def __init__(self, site_config, *, use_timestamps=False):
self._site_config = site_config
self._system = System.create(site_config)
self._current_run = 0
self._timestamp = time.localtime()
self._use_timestamps = use_timestamps

def _makedir(self, *dirs, wipeout=False):
ret = os.path.join(*dirs)
Expand Down Expand Up @@ -110,7 +111,11 @@ def perflogdir(self):

@property
def timestamp(self):
timefmt = self.site_config.get('general/0/timestamp_dirs')
if self._use_timestamps:
timefmt = self.site_config.get('general/0/timestamp_dirs')
else:
timefmt = ''

return time.strftime(timefmt, self._timestamp)

@property
Expand Down Expand Up @@ -192,11 +197,11 @@ def get_default(self, option):
_runtime_context = None


def init_runtime(site_config):
def init_runtime(site_config, **kwargs):
global _runtime_context

if _runtime_context is None:
_runtime_context = RuntimeContext(site_config)
_runtime_context = RuntimeContext(site_config, **kwargs)


def runtime():
Expand Down
15 changes: 8 additions & 7 deletions reframe/core/schedulers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ def filter_nodes_by_state(nodelist, state):
}

return nodelist
nodes[part.fullname] = [n.name for n in nodelist]



class Job(jsonext.JSONSerializable, metaclass=JobMeta):
Expand Down Expand Up @@ -377,7 +375,7 @@ def __init__(self,
self._jobid = None
self._exitcode = None
self._state = None
self._nodelist = None
self._nodelist = []
self._submit_time = None
self._completion_time = None

Expand Down Expand Up @@ -515,7 +513,7 @@ def nodelist(self):
This attribute is supported by the ``local``, ``pbs``, ``slurm``,
``squeue``, ``ssh``, and ``torque`` scheduler backends.

This attribute is :class:`None` if no nodes are assigned to the job
This attribute is an empty list if no nodes are assigned to the job
yet.

The ``squeue`` scheduler backend, i.e., Slurm *without* accounting,
Expand All @@ -531,7 +529,10 @@ def nodelist(self):

.. versionadded:: 2.17

:type: :class:`List[str]` or :class:`None`
.. versionchanged:: 4.7
Default value is the empty list.

:type: :class:`List[str]`
'''
return self._nodelist

Expand All @@ -554,7 +555,7 @@ def prepare(self, commands, environs=None, prepare_cmds=None,
strict_flex=False, **gen_opts):
environs = environs or []
if self.num_tasks is not None and self.num_tasks <= 0:
getlogger().debug(f'[F] Flexible node allocation requested')
getlogger().debug('[F] Flexible node allocation requested')
num_tasks_per_node = self.num_tasks_per_node or 1
min_num_tasks = (-self.num_tasks if self.num_tasks else
num_tasks_per_node)
Expand Down Expand Up @@ -635,7 +636,7 @@ def finished(self):
return done

def __eq__(self, other):
return type(self) == type(other) and self.jobid == other.jobid
return type(self) is type(other) and self.jobid == other.jobid

def __hash__(self):
return hash(self.jobid)
Expand Down
71 changes: 65 additions & 6 deletions reframe/frontend/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@
# that essentially associate environment variables with configuration
# arguments, without having to define a corresponding command line option.

class _Undefined:
pass


# We use a special value for denoting const values that are to be set from the
# configuration default. This placeholder must be used as the `const` argument
# for options with `nargs='?'`. The underlying `ArugmentParser` will use the
vkarak marked this conversation as resolved.
Show resolved Hide resolved
# `const` value as if it were supplied from the command-line thus fooling our
# machinery of environment variables and configuration options overriding any
# defaults. For this reason, we use a unique placeholder so that we can
# distinguish whether this value is a default or actually supplied from the
# command-line.
CONST_DEFAULT = _Undefined()


def _undefined(val):
return val is None or val is CONST_DEFAULT


class _Namespace:
def __init__(self, namespace, option_map):
Expand Down Expand Up @@ -76,7 +94,10 @@ def __getattr__(self, name):
return ret

envvar, _, action, arg_type, default = self.__option_map[name]
if ret is None and envvar is not None:
if ret is CONST_DEFAULT:
default = CONST_DEFAULT

if _undefined(ret) and envvar is not None:
# Try the environment variable
envvar, *delim = envvar.split(maxsplit=2)
delim = delim[0] if delim else ','
Expand Down Expand Up @@ -120,14 +141,14 @@ def update_config(self, site_config):
errors.append(e)
continue

if value is not None:
if not _undefined(value):
site_config.add_sticky_option(confvar, value)

return errors

def __repr__(self):
return (f'{type(self).__name__}({self.__namespace!r}, '
'{self.__option_map})')
f'{self.__option_map})')


class _ArgumentHolder:
Expand All @@ -149,6 +170,12 @@ def __getattr__(self, name):

return getattr(self._holder, name)

def __setattr__(self, name, value):
if name.startswith('_'):
super().__setattr__(name, value)
else:
setattr(self._holder, name, value)

def add_argument(self, *flags, **kwargs):
try:
opt_name = kwargs['dest']
Expand Down Expand Up @@ -233,6 +260,14 @@ def add_argument_group(self, *args, **kwargs):
self._groups.append(group)
return group

def add_mutually_exclusive_group(self, *args, **kwargs):
group = _ArgumentGroup(
self._holder.add_mutually_exclusive_group(*args, **kwargs),
self._option_map
)
self._groups.append(group)
return group

def _resolve_attr(self, attr, namespaces):
for ns in namespaces:
if ns is None:
Expand All @@ -248,7 +283,7 @@ def _update_defaults(self):
for g in self._groups:
self._defaults.__dict__.update(g._defaults.__dict__)

def parse_args(self, args=None, namespace=None):
def parse_args(self, args=None, namespace=None, suppress_required=False):
'''Convert argument strings to objects and return them as attributes of
a namespace.

Expand All @@ -260,7 +295,30 @@ def parse_args(self, args=None, namespace=None):
for it will be looked up first in `namespace` and if not found there,
it will be assigned the default value as specified in its corresponding
`add_argument()` call. If no default value was specified either, the
attribute will be set to `None`.'''
attribute will be set to `None`.

If `suppress_required` is true, required mutually-exclusive groups will
be treated as optional for this parsing operation.
'''

class suppress_required_groups:
'''Temporarily suppress required groups if `suppress_required`
is true.'''
def __init__(this):
this._changed_grp = []

def __enter__(this):
if suppress_required:
for grp in self._groups:
if hasattr(grp, 'required') and grp.required:
this._changed_grp.append(grp)
grp.required = False

return this

def __exit__(this, *args, **kwargs):
for grp in this._changed_grp:
grp.required = True

# Enable auto-completion
argcomplete.autocomplete(self._holder)
Expand All @@ -270,7 +328,8 @@ def parse_args(self, args=None, namespace=None):
# newly parsed options to completely override any options defined in
# namespace. The implementation of `argparse.ArgumentParser` does not
# do this in options with an 'append' action.
options = self._holder.parse_args(args, None)
with suppress_required_groups():
options = self._holder.parse_args(args, None)

# Check if namespace refers to our namespace and take the cmd options
# namespace suitable for ArgumentParser
Expand Down
Loading
Loading