clan_cli,clan_vm_manager: Moved profiler to clan_cli. Add cmd timer to see how long run commands take
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from clan_cli.profiler import profile
|
||||
|
||||
from clan_vm_manager.app import MainApplication
|
||||
from clan_vm_manager.components.profiler import profile
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import os
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -64,7 +63,6 @@ def _init_proc(
|
||||
out_file: Path,
|
||||
proc_name: str,
|
||||
on_except: Callable[[Exception, mp.process.BaseProcess], None] | None,
|
||||
tstart: datetime,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Create a new process group
|
||||
@@ -88,7 +86,6 @@ def _init_proc(
|
||||
linebreak = "=" * 5
|
||||
# Execute the main function
|
||||
print(linebreak + f" {func.__name__}:{pid} " + linebreak, file=sys.stderr)
|
||||
print(f"Spawn overhead time: {datetime.now() - tstart}s", file=sys.stderr)
|
||||
try:
|
||||
func(**kwargs)
|
||||
except Exception as ex:
|
||||
@@ -113,8 +110,6 @@ def spawn(
|
||||
func: Callable,
|
||||
**kwargs: Any,
|
||||
) -> MPProcess:
|
||||
tstart = datetime.now()
|
||||
|
||||
# Decouple the process from the parent
|
||||
if mp.get_start_method(allow_none=True) is None:
|
||||
mp.set_start_method(method="forkserver")
|
||||
@@ -125,7 +120,7 @@ def spawn(
|
||||
# Start the process
|
||||
proc = mp.Process(
|
||||
target=_init_proc,
|
||||
args=(func, out_file, proc_name, on_except, tstart),
|
||||
args=(func, out_file, proc_name, on_except),
|
||||
name=proc_name,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import cProfile
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import pstats
|
||||
import re
|
||||
import weakref
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
# Ensure you have a logger set up for logging exceptions
|
||||
log = logging.getLogger(__name__)
|
||||
explanation = """
|
||||
cProfile Output Columns Explanation:
|
||||
|
||||
- ncalls: The number of calls to the function. This includes both direct and indirect (recursive) calls.
|
||||
|
||||
- tottime: The total time spent in the given function alone, excluding time spent in calls to sub-functions.
|
||||
This measures the function's own overhead and execution time.
|
||||
|
||||
- percall (first instance): Represents the average time spent in the function per call, calculated as tottime divided by ncalls.
|
||||
This value excludes time spent in sub-function calls, focusing on the function's own processing time.
|
||||
|
||||
- cumtime: The cumulative time spent in this function and all the sub-functions it calls (both directly and indirectly).
|
||||
This includes all execution time within the function, from the start of its invocation to its return,
|
||||
including all calls to other functions and the time those calls take.
|
||||
|
||||
- percall (second instance): Represents the average time per call, including time spent in this function and in all sub-function calls.
|
||||
It is calculated as cumtime divided by ncalls, providing an average over all calls that includes all nested function calls.
|
||||
"""
|
||||
|
||||
|
||||
def print_profile(profiler: cProfile.Profile, sortkey: pstats.SortKey) -> None:
|
||||
s = io.StringIO()
|
||||
ps = pstats.Stats(profiler, stream=s)
|
||||
ps.sort_stats(sortkey)
|
||||
ps.print_stats(12)
|
||||
|
||||
# Process the output to trim file paths
|
||||
output_lines = s.getvalue().split("\n")
|
||||
for line in output_lines:
|
||||
try:
|
||||
parts = re.split(r"\s+", line)[
|
||||
1:
|
||||
] # Split on the first space to separate the time from the path
|
||||
fqpath = parts[-1]
|
||||
fpath, line_num = fqpath.split(":")
|
||||
if os.path.sep in fpath: # Check if this looks like it includes a path
|
||||
fpath = trim_path_to_three_levels(fpath)
|
||||
prefix = f"{parts[0]:>7}"
|
||||
prefix += f"{parts[1]:>9}"
|
||||
prefix += f"{parts[2]:>9}"
|
||||
prefix += f"{parts[3]:>9}"
|
||||
prefix += f"{parts[4]:>9}"
|
||||
new_line = f"{prefix:} {fpath}:{line_num}"
|
||||
else:
|
||||
new_line = line
|
||||
except (ValueError, IndexError):
|
||||
new_line = line # If there's no path, leave the line as is
|
||||
print(new_line)
|
||||
|
||||
|
||||
# TODO: Add an RLock for every profiler, currently not thread safe
|
||||
class ProfilerStore:
|
||||
profilers: dict[str, cProfile.Profile]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.profilers = {}
|
||||
self._exit_callback = weakref.finalize(self, self.on_exit)
|
||||
|
||||
def __getitem__(self, func: Callable) -> cProfile.Profile:
|
||||
key = f"{func.__module__}:{func.__name__}"
|
||||
if key not in self.profilers:
|
||||
self.profilers[key] = cProfile.Profile()
|
||||
return self.profilers[key]
|
||||
|
||||
def on_exit(self) -> None:
|
||||
for key, profiler in self.profilers.items():
|
||||
print("=" * 7 + key + "=" * 7)
|
||||
print_profile(profiler, pstats.SortKey.TIME)
|
||||
print_profile(profiler, pstats.SortKey.CUMULATIVE)
|
||||
|
||||
if len(self.profilers) > 0:
|
||||
print(explanation)
|
||||
|
||||
|
||||
def trim_path_to_three_levels(path: str) -> str:
|
||||
parts = path.split(os.path.sep)
|
||||
if len(parts) > 4:
|
||||
return os.path.sep.join(parts[-4:])
|
||||
else:
|
||||
return path
|
||||
|
||||
|
||||
PROFS = ProfilerStore()
|
||||
|
||||
|
||||
def profile(func: Callable) -> Callable:
|
||||
"""
|
||||
A decorator that profiles the decorated function, printing out the profiling
|
||||
results with paths trimmed to three directories deep.
|
||||
"""
|
||||
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
global PROFS
|
||||
profiler = PROFS[func]
|
||||
|
||||
try:
|
||||
profiler.enable()
|
||||
res = func(*args, **kwargs)
|
||||
profiler.disable()
|
||||
except Exception as ex:
|
||||
profiler.disable()
|
||||
raise ex
|
||||
return res
|
||||
|
||||
if os.getenv("PERF", "0") == "1":
|
||||
return wrapper
|
||||
else:
|
||||
return func
|
||||
Reference in New Issue
Block a user