Skip to content

Commit

Permalink
Merge pull request #257 from JiriPavela/feature-polish-import-viewdiff
Browse files Browse the repository at this point in the history
Polish perun import and viewdiff
  • Loading branch information
JiriPavela authored Sep 25, 2024
2 parents 02b625f + 5f0b4e8 commit 752491d
Show file tree
Hide file tree
Showing 25 changed files with 21,423 additions and 669 deletions.
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>]+]'
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

0 comments on commit 752491d

Please sign in to comment.