From 4acae29ea9e00a866ecdfe9166052fe29014e440 Mon Sep 17 00:00:00 2001 From: Jiri Pavela Date: Wed, 9 Sep 2026 15:46:37 +0200 Subject: [PATCH 1/3] Fix lazy loading of expensive libraries Pandas, numpy, and polars were all being imported eagerly when cli_groups was imported. This commit fixes the imports such that they are loaded lazily. --- perun/__init__.py | 3 ++ perun/__init__.pyi | 36 +++++++++++++++++++++ perun/cli_groups/__init__.py | 4 +++ perun/cli_groups/__init__.pyi | 50 +++++++++++++++++++++++++++++ perun/cli_groups/import_cli.py | 11 +++---- perun/cli_groups/meson.build | 1 + perun/cli_groups/showdiff_cli.py | 12 +++---- perun/meson.build | 1 + perun/utils/log.py | 6 ++-- perun/view_diff/report/__init__.pyi | 3 ++ 10 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 perun/__init__.pyi create mode 100644 perun/cli_groups/__init__.pyi diff --git a/perun/__init__.py b/perun/__init__.py index 30547c477..132ae3650 100644 --- a/perun/__init__.py +++ b/perun/__init__.py @@ -17,5 +17,8 @@ from __future__ import annotations import importlib.metadata +import lazy_loader as lazy __version__ = importlib.metadata.version("perun-toolsuite") + +__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) diff --git a/perun/__init__.pyi b/perun/__init__.pyi new file mode 100644 index 000000000..df6a91a47 --- /dev/null +++ b/perun/__init__.pyi @@ -0,0 +1,36 @@ +from . import ( + check as check, + cli_groups as cli_groups, + collect as collect, + deltadebugging as deltadebugging, + fuzz as fuzz, + logic as logic, + postprocess as postprocess, + profiles as profiles, + scripts as scripts, + selection as selection, + templates as templates, + testing as testing, + thirdparty as thirdparty, + utils as utils, + vcs as vcs, + view as view, + view_diff as view_diff, + workload as workload, +) + +from .cli import ( + cli as cli, + init as init, + add as add, + remove as remove, + log as log, + status as status, + show as show, + postprocessby as postprocessby, + fuzz_cmd as fuzz_cmd, + deltadebugging as deltadebugging, + launch_cli_in_dev_mode as launch_cli_in_dev_mode, + launch_cli_safely as launch_cli_safely, + launch_cli as launch_cli, +) diff --git a/perun/cli_groups/__init__.py b/perun/cli_groups/__init__.py index b127cd763..828a2f353 100644 --- a/perun/cli_groups/__init__.py +++ b/perun/cli_groups/__init__.py @@ -1 +1,5 @@ """Collection of CLI groups that are not part of the standalone commands""" + +import lazy_loader as lazy + +__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) diff --git a/perun/cli_groups/__init__.pyi b/perun/cli_groups/__init__.pyi new file mode 100644 index 000000000..2be271648 --- /dev/null +++ b/perun/cli_groups/__init__.pyi @@ -0,0 +1,50 @@ +from .check_cli import ( + check_group as check_group, + check_head as check_head, + check_all as check_all, + check_profiles as check_profiles, +) + +from .collect_cli import collect as collect + +from .config_cli import ( + config as config, + config_get as config_get, + config_set as config_set, + config_edit as config_edit, + config_reset as config_reset, +) + +from .import_cli import ( + import_group as import_group, + perf_group as perf_group, + from_binary as from_binary, + from_text as from_text, + from_stacks as from_stacks, + elk_group as elk_group, + from_json as from_json, +) + +from .run_cli import ( + run as run, + matrix as matrix, + job as job, +) + +from .shared_options import ( + flamegraph_cli_options as flamegraph_cli_options, + profile_postprocess_cli_options as profile_postprocess_cli_options, + profile_aggregation_cli_option as profile_aggregation_cli_options, +) + +from .showdiff_cli import ( + perun_profile_list_options as perun_profile_list_options, + common_html_options as common_html_options, + flamegraph_diff_cli_options as flamegraph_diff_cli_options, + showdiff_group as showdiff_group, + short_diff as short_diff, + flamegraph_diff as flamegraph_diff, + report_group as report_group, + report_native as report_native, + report_folded as report_folded, +) diff --git a/perun/cli_groups/import_cli.py b/perun/cli_groups/import_cli.py index c2d4c7f97..21c7f1585 100755 --- a/perun/cli_groups/import_cli.py +++ b/perun/cli_groups/import_cli.py @@ -10,8 +10,7 @@ # Perun Imports from perun.logic import commands, config -from perun.profiles import native as profile -from perun.profiles.conversions import perf_native, elk_native +from perun.profiles import native as profile, conversions from perun.utils.common import cli_kit @@ -187,7 +186,7 @@ def from_binary(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> that combines the --stats-headers option and profile entries. """ kwargs.update(ctx.obj) - perf_native.import_perf_from_record(import_entries, **kwargs) + conversions.perf_native.import_perf_from_record(import_entries, **kwargs) @perf_group.command("script") @@ -214,7 +213,7 @@ def from_text(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> N that combines the --stats-headers option and profile entries. """ kwargs.update(ctx.obj) - perf_native.import_perf_from_script(import_entries, **kwargs) + conversions.perf_native.import_perf_from_script(import_entries, **kwargs) @perf_group.command("stack") @@ -242,7 +241,7 @@ def from_stacks(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> that combines the --stats-headers option and profile entries. """ kwargs.update(ctx.obj) - perf_native.import_perf_from_stack(import_entries, **kwargs) + conversions.perf_native.import_perf_from_stack(import_entries, **kwargs) @import_group.group("elk") @@ -272,4 +271,4 @@ def from_json(ctx: click.Context, import_entries: list[str], **kwargs: Any) -> N Each import entry may specify a JSON path 'file_path.json'. """ kwargs.update(ctx.obj) - elk_native.import_elk_from_json(import_entries, **kwargs) + conversions.elk_native.import_elk_from_json(import_entries, **kwargs) diff --git a/perun/cli_groups/meson.build b/perun/cli_groups/meson.build index de396c8e3..2590a7e28 100644 --- a/perun/cli_groups/meson.build +++ b/perun/cli_groups/meson.build @@ -2,6 +2,7 @@ perun_cli_groups_dir = perun_dir / 'cli_groups' perun_cli_groups_files = files( '__init__.py', + '__init__.pyi', 'check_cli.py', 'collect_cli.py', 'config_cli.py', diff --git a/perun/cli_groups/showdiff_cli.py b/perun/cli_groups/showdiff_cli.py index c771dcfe8..14d52bc41 100644 --- a/perun/cli_groups/showdiff_cli.py +++ b/perun/cli_groups/showdiff_cli.py @@ -21,8 +21,8 @@ DEFAULT_TRACE_THRESHOLD, HeaderDisplayStyle, ) -from perun.view_diff import flamegraph, short -from perun.view_diff.report import native, folded +from perun import view_diff +from perun.view_diff import report if TYPE_CHECKING: from perun.profiles.native import Profile @@ -178,7 +178,7 @@ def short_diff(profile_list: tuple[Profile, Profile], *_: Any, **kwargs: Any) -> Supports only perun-native profiles. """ - short.compare_profiles(*profile_list, **kwargs) + view_diff.short.compare_profiles(*profile_list, **kwargs) @showdiff_group.command("flamegraph") @@ -221,7 +221,7 @@ def flamegraph_diff(profile_list: tuple[Profile, Profile], *_: Any, **kwargs: An Supports only perun-native profiles. """ - flamegraph.generate_flamegraph_difference(*profile_list, **kwargs) + view_diff.flamegraph.generate_flamegraph_difference(*profile_list, **kwargs) @showdiff_group.group("report") @@ -327,7 +327,7 @@ def report_native( perun showdiff report native -m HEAD~1 0@i 1@i """ kwargs.update(ctx.obj) - native.generate_report_from_native_profiles(*profile_list, **kwargs) + report.native.generate_report_from_native_profiles(*profile_list, **kwargs) @report_group.command("folded") @@ -507,4 +507,4 @@ def report_folded(ctx: click.Context, baseline: str, target: str, **kwargs: Any) """ kwargs.update(ctx.obj) - folded.generate_report_from_folded_profiles(baseline, target, **kwargs) + report.folded.generate_report_from_folded_profiles(baseline, target, **kwargs) diff --git a/perun/meson.build b/perun/meson.build index 1dd3dd459..206b57157 100644 --- a/perun/meson.build +++ b/perun/meson.build @@ -1,6 +1,7 @@ perun_files = files( 'cli.py', '__init__.py', + '__init__.pyi', 'py.typed', ) diff --git a/perun/utils/log.py b/perun/utils/log.py index 1b4fe21cd..5cb7b2c61 100644 --- a/perun/utils/log.py +++ b/perun/utils/log.py @@ -17,8 +17,6 @@ import traceback # Third-Party Imports -import numpy as np -import progressbar import termcolor # Perun Imports @@ -713,6 +711,8 @@ def aggregate_intervals( :return: list of the aggregated partial intervals to print """ # Fixme: This is baaaad. But the partial intervals are somewhat broken (sometimes list, sometimes narray) + import numpy as np + intervals = np.array(input_intervals) if isinstance(input_intervals, list) else input_intervals def get_indices_of_intervals() -> Iterable[tuple[int, int]]: @@ -867,6 +867,8 @@ def progress(collection: Iterable[T], description: str = "") -> Iterable[T]: :param collection: any iterable :param description: tag on the left side of the output of the bar """ + import progressbar + widgets: list[progressbar.widgets.WidgetBase | str] = [ (description + ": ") if description else "", progressbar.Percentage(), diff --git a/perun/view_diff/report/__init__.pyi b/perun/view_diff/report/__init__.pyi index 26246b8e1..1c1e4a083 100644 --- a/perun/view_diff/report/__init__.pyi +++ b/perun/view_diff/report/__init__.pyi @@ -1,3 +1,6 @@ +from . import folded as folded +from . import native as native + from .core import ( ProfileMisc as ProfileMisc, SelectionRow as SelectionRow, From 490d22d91ce13f8f7ae92a05656efe2c91b7ab5a Mon Sep 17 00:00:00 2001 From: Jiri Pavela Date: Wed, 9 Sep 2026 16:55:46 +0200 Subject: [PATCH 2/3] Optimize the 'escape_flamegraph_svg' function Previously, this function did more than 60 individual regex substitution passes. This commit merges eligible passes into cca 10 passes, which greatly improves performance when generating reports from small profiles. --- perun/view/flamegraph/grid.py | 190 ++++++++++++++++++---------------- 1 file changed, 98 insertions(+), 92 deletions(-) diff --git a/perun/view/flamegraph/grid.py b/perun/view/flamegraph/grid.py index e51215cbc..11b38bf94 100644 --- a/perun/view/flamegraph/grid.py +++ b/perun/view/flamegraph/grid.py @@ -25,6 +25,62 @@ from types import TracebackType +# SVG-escaping patterns. +# Function patterns. +_FUNC_PATTERN = re.compile( + r"(?\s*\n<") +_GET_ELEMENTS_PATTERN = re.compile(r'getElementsByTagName\("svg"\)\[0\]') +_SVG_TAG_PATTERN = re.compile(r'\s*\n<", r"><"), - ( - r"getElementsByTagName\(\"svg\"\)\[0\]", - f'getElementsByClassName("svg-content")[{tag_index}]', - ), - (r"document.", f"{tag}_svg."), - (f"{tag}_svg.createElementNS", "document.createElementNS"), - ( - f"({tag}_(svg|details|detailsName|detailsIncl|detailsExcl|matchedHoverCount|matchedHoverIncl|matchedHoverExcl|matchedSearchCount|matchedSearchIncl|matchedSearchExcl|nameTypeLabel|inclusiveLabel|exclusiveLabel|matchedHoverLabel|matchedSearchLabel|searchbtn|matchedtxt|matchedHoverTxt|ignorecaseBtn|unzoombtn)) = {tag}_svg.", - "\\1 = document.", - ), - # Huge thanks to following article: - # https://chartio.com/resources/tutorials/how-to-resize-an-svg-when-the-window-is-resized-in-d3-js/ - # Which helped to solve the issue with non-resizable flamegraphs - ( - ' str: + s = m.group(0) + if s[0] == '"': + return f'"{tag}_' + s[1:] + if s[0] == "#": + return f"#{tag}_" + s[1:] + return f"{tag}_" + s + + content = _LITERAL_PATTERN.sub(_literal_repl, content) + + content = re.sub(r"matched", f"{tag}_matched", content) + + content = _SVG_VAR_PATTERN.sub(rf"{tag}_svg\1", content) + content = _WS_PATTERN.sub("><", content) + content = _GET_ELEMENTS_PATTERN.sub( + f'getElementsByClassName("svg-content")[{tag_index}]', content + ) + + # Replace 'document.' references, then revert specific cases. + content = re.sub(r"document.", f"{tag}_svg.", content) + content = content.replace(f"{tag}_svg.createElementNS", "document.createElementNS") + content = re.sub( + f"({tag}_(svg|details|detailsName|detailsIncl|detailsExcl" + f"|matchedHoverCount|matchedHoverIncl|matchedHoverExcl" + f"|matchedSearchCount|matchedSearchIncl|matchedSearchExcl" + f"|nameTypeLabel|inclusiveLabel|exclusiveLabel" + f"|matchedHoverLabel|matchedSearchLabel" + f"|searchbtn|matchedtxt|matchedHoverTxt" + f"|ignorecaseBtn|unzoombtn)) = {tag}_svg.", + r"\1 = document.", + content, + ) + # Huge thanks to following article: + # https://chartio.com/resources/tutorials/how-to-resize-an-svg-when-the-window-is-resized-in-d3-js/ + # Which helped to solve the issue with non-resizable flamegraphs + content = _SVG_TAG_PATTERN.sub( + ' Date: Wed, 9 Sep 2026 18:09:54 +0200 Subject: [PATCH 3/3] Finishing touches --- perun/__init__.py | 3 --- perun/__init__.pyi | 36 ------------------------- perun/cli_groups/__init__.pyi | 9 +++++++ perun/fuzz/__init__.pyi | 6 +++++ perun/meson.build | 1 - perun/profiles/conversions/__init__.pyi | 10 +++++++ perun/profiles/folded/__init__.pyi | 3 +++ perun/profiles/native/__init__.pyi | 4 +++ perun/profiles/polars/__init__.pyi | 4 +++ perun/view/flamegraph/__init__.pyi | 4 +++ perun/view_diff/__init__.pyi | 2 ++ 11 files changed, 42 insertions(+), 40 deletions(-) delete mode 100644 perun/__init__.pyi diff --git a/perun/__init__.py b/perun/__init__.py index 132ae3650..30547c477 100644 --- a/perun/__init__.py +++ b/perun/__init__.py @@ -17,8 +17,5 @@ from __future__ import annotations import importlib.metadata -import lazy_loader as lazy __version__ = importlib.metadata.version("perun-toolsuite") - -__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__) diff --git a/perun/__init__.pyi b/perun/__init__.pyi deleted file mode 100644 index df6a91a47..000000000 --- a/perun/__init__.pyi +++ /dev/null @@ -1,36 +0,0 @@ -from . import ( - check as check, - cli_groups as cli_groups, - collect as collect, - deltadebugging as deltadebugging, - fuzz as fuzz, - logic as logic, - postprocess as postprocess, - profiles as profiles, - scripts as scripts, - selection as selection, - templates as templates, - testing as testing, - thirdparty as thirdparty, - utils as utils, - vcs as vcs, - view as view, - view_diff as view_diff, - workload as workload, -) - -from .cli import ( - cli as cli, - init as init, - add as add, - remove as remove, - log as log, - status as status, - show as show, - postprocessby as postprocessby, - fuzz_cmd as fuzz_cmd, - deltadebugging as deltadebugging, - launch_cli_in_dev_mode as launch_cli_in_dev_mode, - launch_cli_safely as launch_cli_safely, - launch_cli as launch_cli, -) diff --git a/perun/cli_groups/__init__.pyi b/perun/cli_groups/__init__.pyi index 2be271648..0651caf0c 100644 --- a/perun/cli_groups/__init__.pyi +++ b/perun/cli_groups/__init__.pyi @@ -1,3 +1,12 @@ +from . import check_cli as check_cli +from . import collect_cli as collect_cli +from . import config_cli as config_cli +from . import import_cli as import_cli +from . import run_cli as run_cli +from . import shared_options as shared_options +from . import showdiff_cli as showdiff_cli +from . import utils_cli as utils_cli + from .check_cli import ( check_group as check_group, check_head as check_head, diff --git a/perun/fuzz/__init__.pyi b/perun/fuzz/__init__.pyi index 1d99baf33..4f639b810 100644 --- a/perun/fuzz/__init__.pyi +++ b/perun/fuzz/__init__.pyi @@ -1 +1,7 @@ +from . import filesystem as filesystem +from . import filetype as filetype +from . import helpers as helpers +from . import interpret as interpret +from . import randomizer as randomizer + from .factory import run_fuzzing_for_command as run_fuzzing_for_command diff --git a/perun/meson.build b/perun/meson.build index 206b57157..1dd3dd459 100644 --- a/perun/meson.build +++ b/perun/meson.build @@ -1,7 +1,6 @@ perun_files = files( 'cli.py', '__init__.py', - '__init__.pyi', 'py.typed', ) diff --git a/perun/profiles/conversions/__init__.pyi b/perun/profiles/conversions/__init__.pyi index b5a9ed45a..22d17b983 100644 --- a/perun/profiles/conversions/__init__.pyi +++ b/perun/profiles/conversions/__init__.pyi @@ -1,3 +1,13 @@ +from . import ( + elk_native as elk_native, + folded_polars as folded_polars, + native_folded as native_folded, + native_pandas as native_pandas, + native_polars as native_polars, + perf_native as perf_native, + polars_folded as polars_folded, +) + from .elk_native import import_elk_from_json as import_elk_from_json from .folded_polars import ( diff --git a/perun/profiles/folded/__init__.pyi b/perun/profiles/folded/__init__.pyi index a49f6062e..06e0d87f1 100644 --- a/perun/profiles/folded/__init__.pyi +++ b/perun/profiles/folded/__init__.pyi @@ -1,3 +1,6 @@ +from . import parser as parser +from . import postprocess as postprocess + from .parser import ( parse_resources_from_stream as parse_resources_from_stream, parse_events_from_stream as parse_events_from_stream, diff --git a/perun/profiles/native/__init__.pyi b/perun/profiles/native/__init__.pyi index 2eedf0776..406d56b44 100644 --- a/perun/profiles/native/__init__.pyi +++ b/perun/profiles/native/__init__.pyi @@ -1,3 +1,7 @@ +from . import factory as factory +from . import helpers as helpers +from . import query as query + from .factory import ( Profile as Profile, pass_profile as pass_profile, diff --git a/perun/profiles/polars/__init__.pyi b/perun/profiles/polars/__init__.pyi index ca7897d00..bd20779a3 100644 --- a/perun/profiles/polars/__init__.pyi +++ b/perun/profiles/polars/__init__.pyi @@ -1,3 +1,7 @@ +from . import convert as convert +from . import parser as parser +from . import structs as structs + from .convert import ( create_polars_pair_profile as create_polars_pair_profile, merge_and_filter_polars_profiles as merge_and_filter_polars_profiles, diff --git a/perun/view/flamegraph/__init__.pyi b/perun/view/flamegraph/__init__.pyi index 69c856647..928982063 100644 --- a/perun/view/flamegraph/__init__.pyi +++ b/perun/view/flamegraph/__init__.pyi @@ -1,3 +1,7 @@ +from . import core as core +from . import grid as grid +from . import run as run + from .core import ( generate_flamegraph as generate_flamegraph, generate_title as generate_title, diff --git a/perun/view_diff/__init__.pyi b/perun/view_diff/__init__.pyi index cbdedc16a..019fc7284 100644 --- a/perun/view_diff/__init__.pyi +++ b/perun/view_diff/__init__.pyi @@ -1,4 +1,6 @@ from . import report as report +from . import short as short +from . import flamegraph as flamegraph from .chatbot import generate_initial_prompt as generate_initial_prompt