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

Polish perun import and viewdiff #257

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
141 changes: 107 additions & 34 deletions perun/cli_groups/import_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
import click

# Perun Imports
from perun.logic import commands
from perun.logic import commands, config
from perun.profile import imports
from perun.utils.common import cli_kit


@click.group("import")
@click.option(
"--machine-info",
"-i",
type=click.Path(resolve_path=True, readable=True),
type=click.Path(),
default="",
help="Imports machine info from file in JSON format (by default, machine info is loaded from "
"the current host). You can use `utils/generate_machine_info.sh` script to generate the "
"machine info file.",
Expand All @@ -26,7 +28,9 @@
"--import-dir",
"-d",
type=click.Path(resolve_path=True, readable=True),
help="Specifies the directory to import profiles from.",
callback=cli_kit.set_config_option_from_flag(config.runtime, "import.dir"),
help="Specifies the directory from which to import profiles and other files (e.g., stats, "
"machine info, ...) that are provided as relative paths (default = ./).",
)
@click.option(
"--minor-version",
Expand All @@ -37,23 +41,30 @@
help="Specifies the head minor version, for which the profiles will be imported.",
)
@click.option(
"--stats-info",
"--stats-headers",
"-t",
nargs=1,
default=None,
metavar="<stat1-description,...>",
help="Describes the stats associated with the imported profiles. Please see the import "
"documentation for details regarding the stat description format.",
default="",
metavar="[STAT_HEADER+]",
help="Describes the stats headers associated with imported profiles specified directly in CLI. "
"A stats header has the form of 'NAME[|COMPARISON_TYPE[|UNIT[|AGGREGATE_BY[|DESCRIPTION]]]]'.",
)
@click.option(
"--metadata",
"-md",
multiple=True,
metavar="['KEY|VALUE|[DESCRIPTION]'] or [FILE.json]",
help="Describes a single metadata entry associated with the imported profiles as a "
"'key|value[|description]' string, or a JSON file that may contain multiple metadata entries "
"that will have its keys flattened. The --metadata option may be specified multiple times.",
)
@click.option(
"--cmd",
"-c",
nargs=1,
default="",
help=(
"Command that was being profiled. Either corresponds to some"
" script, binary or command, e.g. ``./mybin`` or ``perun``."
),
help="Command that was being profiled. Either corresponds to some script, binary or command, "
"e.g. ``./mybin`` or ``perun``.",
)
@click.option(
"--workload",
Expand All @@ -66,12 +77,17 @@
"--save-to-index",
"-s",
is_flag=True,
help="Saves the imported profile to index.",
default=False,
help="Saves the imported profile to index.",
)
@click.pass_context
def import_group(ctx: click.Context, **kwargs: Any) -> None:
"""Imports Perun profiles from different formats"""
"""Imports Perun profiles from different formats.

If the --import-dir parameter is specified, relative file paths will be prefixed with the
import directory path (with the default value being the current working directory).
Absolute file paths ignore the import directory.
"""
commands.try_init()
ctx.obj = kwargs

Expand All @@ -89,15 +105,16 @@ def perf_group(ctx: click.Context, **kwargs: Any) -> None:

This supports either profiles collected in:

1. Binary format: e.g., `collected.data` files, that are results of `perf record`
2. Text format: result of `perf script` that parses the binary into user-friendly and
parsing-friendly text format
1. Binary format: e.g., `collected.data` files, that are results of `perf record`

2. Text format: result of `perf script` that parses the binary into user-friendly and
parsing-friendly text format
"""
ctx.obj.update(kwargs)


@perf_group.command("record")
@click.argument("imported", nargs=-1, required=True)
@click.argument("import_entries", nargs=-1, required=True)
@click.pass_context
@click.option(
"--with-sudo",
Expand All @@ -106,30 +123,83 @@ def perf_group(ctx: click.Context, **kwargs: Any) -> None:
help="Runs the conversion of the data in sudo mode.",
default=False,
)
def from_binary(ctx: click.Context, imported: list[str], **kwargs: Any) -> None:
"""Imports Perun profiles from binary generated by `perf record` command"""
def from_binary(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> None:
"""Imports Perun profiles from binary generated by `perf record` command.

Multiple import entries may be specified; an import entry is either a profile entry

'profile_path[,<exit code>[,<stat value>]+]'

where each stat value corresponds to a stats header specified in the --stats-headers option,
or a CSV file entry

'file.csv'

where the CSV file is in the format

#Profile,Exit_code[,stat-header1]+
profile_path[,<exit code>[,<stat value>]+]
...

that combines the --stats-headers option and profile entries.
"""
kwargs.update(ctx.obj)
imports.import_perf_from_record(imported, **kwargs)
imports.import_perf_from_record(import_entries, **kwargs)


@perf_group.command("script")
@click.argument("imported", type=str, nargs=-1, required=True)
@click.argument("import_entries", type=str, nargs=-1, required=True)
@click.pass_context
def from_text(ctx: click.Context, imported: list[str], **kwargs: Any) -> None:
"""Import Perun profiles from output generated by `perf script` command"""
def from_text(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> None:
"""Import Perun profiles from output generated by `perf script` command.

Multiple import entries may be specified; an import entry is either a profile entry

'profile_path[,<exit code>[,<stat value>]+]'

where each stat value corresponds to a stats header specified in the --stats-headers option,
or a CSV file entry

'file.csv'

where the CSV file is in the format

#Profile,Exit_code[,stat-header1]+
profile_path[,<exit code>[,<stat value>]+]
...

that combines the --stats-headers option and profile entries.
"""
kwargs.update(ctx.obj)
imports.import_perf_from_script(imported, **kwargs)
imports.import_perf_from_script(import_entries, **kwargs)


@perf_group.command("stack")
@click.argument("imported", type=str, nargs=-1, required=True)
@click.argument("import_entries", type=str, nargs=-1, required=True)
@click.pass_context
def from_stacks(ctx: click.Context, imported: list[str], **kwargs: Any) -> None:
def from_stacks(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> None:
"""Import Perun profiles from output generated by `perf script | stackcollapse-perf.pl`
command
command.

Multiple import entries may be specified; an import entry is either a profile entry

'profile_path[,<exit code>[,<stat value>]+]'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? And can you maybe write an example how his is run? That's better readable then nested parenthesized specification.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parentheses represent optional parts of the import entries. This change was done so that (a) CSV and CLI specification of profiles to import is unified, i.e., it is now possible to specify exactly the same import parameters using both CSV files and CLI, and (b) at least exit code may be specified on CLI alongside the profiles to import. However, the interface is backwards-compatible and simple profile paths, e.g., import.stack.gz may still be specified. An example input containing both exit code and some stats values (corresponding to some specified stats headers defined in option --stats-headers) is, e.g., 'import.stack.gz,0,18511.379883,367'.


where each stat value corresponds to a stats header specified in the --stats-headers option,
or a CSV file entry

'file_path.csv'

where the CSV file is in the format

#Profile,Exit_code[,stat-header1]+
profile_path[,<exit code>[,<stat value>]+]
...

that combines the --stats-headers option and profile entries.
"""
kwargs.update(ctx.obj)
imports.import_perf_from_stack(imported, **kwargs)
imports.import_perf_from_stack(import_entries, **kwargs)


@import_group.group("elk")
Expand All @@ -145,15 +215,18 @@ def elk_group(ctx: click.Context, **kwargs: Any) -> None:

The command supports profiles collected in:

1. JSON format: files, that are extracted from ELK or are stored using format compatible with ELK.
1. JSON format: files extracted from ELK or stored using format compatible with ELK.
"""
ctx.obj.update(kwargs)


@elk_group.command("json")
@click.argument("imported", nargs=-1, required=True)
@click.argument("import_entries", nargs=-1, required=True)
@click.pass_context
def from_json(ctx: click.Context, imported: list[str], **kwargs: Any) -> None:
"""Imports Perun profiles from json compatible with elk infrastructure"""
def from_json(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> None:
"""Imports Perun profiles from JSON compatible with elk infrastructure.

Each import entry may specify a JSON path 'file_path.json'.
"""
kwargs.update(ctx.obj)
imports.import_elk_from_json(imported, **kwargs)
imports.import_elk_from_json(import_entries, **kwargs)
18 changes: 17 additions & 1 deletion perun/profile/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# Perun Imports
from perun.logic import config
from perun.postprocess.regression_analysis import regression_models
from perun.profile import convert, query
from perun.profile import convert, query, stats, helpers
from perun.utils import log
from perun.utils.common import common_kit
import perun.check.detection_kit as detection
Expand Down Expand Up @@ -455,6 +455,22 @@ def all_snapshots(self) -> Iterable[tuple[int, list[dict[str, Any]]]]:
for i in range(0, maximal_snapshot + 1):
yield i, snapshot_map[i]

def all_stats(self) -> Iterable[stats.ProfileStat]:
"""Iterates through all the stats records in the profile.

:return: iterable of all stats records
"""
for stat in self._storage.get("stats", {}):
yield stats.ProfileStat.from_profile(stat)

def all_metadata(self) -> Iterable[helpers.ProfileMetadata]:
"""Iterates through all the metadata records in the profile.

:return: iterable of all metadata records
"""
for entry in self._storage.get("metadata", {}):
yield helpers.ProfileMetadata.from_profile(entry)

# TODO: discuss the intent of __len__ and possibly merge?
def resources_size(self) -> int:
"""Returns the number of resources stored in the internal storage.
Expand Down
81 changes: 37 additions & 44 deletions perun/profile/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
from __future__ import annotations

# Standard Imports
from typing import Any, TYPE_CHECKING, ClassVar
import dataclasses
import json
import operator
import os
import re
import time
from dataclasses import dataclass
from typing import Any, TYPE_CHECKING

# Third-Party Imports

Expand Down Expand Up @@ -612,49 +612,42 @@ def is_compatible_with_profile(self, profile: profiles.Profile) -> bool:
]


@dataclass
class ProfileStat:
ALLOWED_ORDERING: ClassVar[dict[str, bool]] = {
"higher_is_better": True,
"lower_is_better": False,
}
@dataclasses.dataclass
class ProfileMetadata:
"""A representation of a single profile metadata entry.

:ivar name: the name (key) of the metadata entry
:ivar value: the value of the metadata entry
:ivar description: detailed description of the metadata entry
"""

name: str
unit: str = "#"
ordering: bool = True
tooltip: str = ""
value: int | float = 0.0
value: str | float
description: str = ""

@classmethod
def from_string(
cls,
name: str = "empty",
unit: str = "#",
ordering: str = "higher_is_better",
tooltip: str = "",
*_: Any,
) -> ProfileStat:
if name == "empty":
# Invalid stat specification, warn
perun_log.warn("Empty profile stat specification. Creating a dummy 'empty' stat.")
if ordering not in cls.ALLOWED_ORDERING:
# Invalid stat ordering, warn
perun_log.warn(
f"Unknown stat ordering: {ordering}. Please choose one of "
f"({', '.join(cls.ALLOWED_ORDERING.keys())}). "
f"Using the default stat ordering value."
)
ordering_bool = ProfileStat.ordering
else:
ordering_bool = cls.ALLOWED_ORDERING[ordering]
return cls(name, unit, ordering_bool, tooltip)

def get_normalized_tooltip(self) -> str:
# Find the string representation of the ordering to use in the tooltip
ordering: str = ""
for str_desc, bool_repr in self.ALLOWED_ORDERING.items():
if bool_repr == self.ordering:
ordering = str_desc.replace("_", " ")
if self.tooltip:
return f"{self.tooltip} ({ordering})"
return ordering
def from_string(cls, metadata: str) -> ProfileMetadata:
"""Constructs a ProfileMetadata object from a string representation.

:param metadata: the string representation of a metadata entry

:return: the constructed ProfileMetadata object
"""
return cls(*metadata.split("|"))

@classmethod
def from_profile(cls, metadata: dict[str, Any]) -> ProfileMetadata:
"""Constructs a ProfileMetadata object from a dictionary representation used in Profile.

:param metadata: the dictionary representation of a metadata entry

:return: the constructed ProfileMetadata object
"""
return cls(**metadata)

def as_tuple(self) -> tuple[str, str | float, str]:
"""Converts the metadata object into a tuple.

:return: the tuple representation of a metadata entry
"""
return self.name, self.value, self.description
Loading
Loading