File: debug.py

package info (click to toggle)
sphinx-needs 5.1.0%2Bdfsg-6
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 12,108 kB
  • sloc: python: 21,148; javascript: 187; makefile: 95; sh: 29; xml: 10
file content (189 lines) | stat: -rw-r--r-- 6,458 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
"""
Contains debug features to track down
runtime and other problems with Sphinx-Needs
"""

from __future__ import annotations

import inspect
import json
import os.path
from datetime import datetime
from functools import wraps
from pathlib import Path
from timeit import default_timer as timer  # Used for timing measurements
from typing import Any, Callable, TypeVar

from jinja2 import Environment, PackageLoader, select_autoescape
from sphinx.application import Sphinx

TIME_MEASUREMENTS: dict[str, Any] = {}  # Stores the timing results
EXECUTE_TIME_MEASUREMENTS = (
    False  # Will be used to de/activate measurements. Set during a Sphinx Event
)

START_TIME = 0.0

T = TypeVar("T", bound=Callable[..., Any])


def measure_time(
    category: str | None = None, source: str = "internal", name: str | None = None
) -> Callable[[T], T]:
    """
    Decorator for measuring the needed execution time of a specific function.

    It measures:

    * Amount of executions
    * Overall time consumed
    * Average time of an execution as `avg`
    * Minimum time of an execution as `min`
    * Maximum time of an execution as `max`

    For `max` also the used function parameters are stored as string values, to make
    it easier to reproduce the maximum case.

    Usage as decorator::

        from sphinx_needs.utils import measure_time

        @measure_time('my_category')
        def my_cool_function(a, b,c ):
            # does something

    :param category: Name of a category, which helps to cluster the measured functions.
    :param source: Should be "internal" or "user". Used to easily structure function written by user.
    :param name: Name to use for the measured. If not given, the function name is used.
    """

    def inner(func: T) -> T:
        @wraps(func)
        def wrapper(*args: list[object], **kwargs: dict[object, object]) -> Any:
            """
            Wrapper function around a given/decorated function, which cares about measurement and storing the result

            :param args: Arguments for the original function
            :param kwargs: Keyword arguments for the original function
            """
            if not EXECUTE_TIME_MEASUREMENTS:
                return func(*args, **kwargs)

            start = timer()
            # Execute original function
            result = func(*args, **kwargs)
            end = timer()

            runtime = end - start

            mt_name = func.__name__ if name is None else name

            mt_id = f"{category}_{func.__name__}"

            if mt_id not in TIME_MEASUREMENTS:
                TIME_MEASUREMENTS[mt_id] = {
                    "name": mt_name,
                    "category": category,
                    "source": source,
                    "doc": func.__doc__,
                    "file": inspect.getfile(func),
                    "line": inspect.getsourcelines(func)[1],
                    "amount": 0,
                    "overall": 0,
                    "avg": None,
                    "min": None,
                    "max": None,
                    "min_max_spread": None,
                    "max_params": {"args": [], "kwargs": {}},
                }

            runtime_dict = TIME_MEASUREMENTS[mt_id]

            runtime_dict["amount"] += 1
            runtime_dict["overall"] += runtime

            if runtime_dict["min"] is None or runtime < runtime_dict["min"]:
                runtime_dict["min"] = runtime

            if runtime_dict["max"] is None or runtime > runtime_dict["max"]:
                runtime_dict["max"] = runtime
                runtime_dict["max_params"] = {  # Store parameters as a shorten string
                    "args": str([str(arg)[:80] for arg in args]),
                    "kwargs": str(
                        {key: str(value)[:80] for key, value in kwargs.items()}
                    ),
                }
            runtime_dict["min_max_spread"] = (
                runtime_dict["max"] / runtime_dict["min"] * 100
            )
            runtime_dict["avg"] = runtime_dict["overall"] / runtime_dict["amount"]
            return result

        return wrapper  # type: ignore

    return inner


def measure_time_func(
    func: T,
    category: str | None = None,
    source: str = "internal",
    name: str | None = None,
) -> T:
    """Wrapper for measuring the needed execution time of a specific function.

    Usage as function::

        from sphinx_needs.utils import measure_time

        # Old call: my_cool_function(a,b,c)
        new_func = measure_time_func('my_category', func=my_cool_function)
        new_func(a,b,c)
    """
    return measure_time(category, source, name)(func)


def _print_timing_results(app: Sphinx) -> None:
    for value in TIME_MEASUREMENTS.values():
        print(value["name"])
        print(f" amount:  {value['amount']}")
        print(f" overall: {value['overall']:2f}")
        print(f" avg:     {value['avg']:2f}")
        print(f" max:     {value['max']:2f}")
        print(f" min:     {value['min']:2f} \n")


def _store_timing_results_json(app: Sphinx, build_data: dict[str, Any]) -> None:
    json_result_path = os.path.join(str(app.outdir), "debug_measurement.json")

    data = {"build": build_data, "measurements": TIME_MEASUREMENTS}

    with open(json_result_path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=4)
    print(f"Timing measurement results (JSON) stored under {json_result_path}")


def _store_timing_results_html(app: Sphinx, build_data: dict[str, Any]) -> None:
    jinja_env = Environment(
        loader=PackageLoader("sphinx_needs"), autoescape=select_autoescape()
    )
    template = jinja_env.get_template("time_measurements.html")
    out_file = Path(str(app.outdir)) / "debug_measurement.html"
    with open(out_file, "w", encoding="utf-8") as f:
        f.write(template.render(data=TIME_MEASUREMENTS, build_data=build_data))
    print(f"Timing measurement report (HTML) stored under {out_file}")


def process_timing(app: Sphinx, _exception: Exception | None) -> None:
    if EXECUTE_TIME_MEASUREMENTS:
        build_data = {
            "project": app.config["project"],
            "start": START_TIME,
            "end": timer(),
            "duration": timer() - START_TIME,
            "timestamp": datetime.now().isoformat(),
        }

        _print_timing_results(app)
        _store_timing_results_json(app, build_data)
        _store_timing_results_html(app, build_data)