mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
173 lines
6.1 KiB
Python
173 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import threading
|
|
from collections.abc import Generator, Iterator, Mapping
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from types import CodeType, FrameType, FunctionType, MappingProxyType
|
|
from typing import Final
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FunctionTraceEvent:
|
|
id: int
|
|
parent_id: int | None
|
|
function: str
|
|
module_path: str | None = None
|
|
file: str | None = None
|
|
line: int | None = None
|
|
|
|
@property
|
|
def raw(self) -> str:
|
|
location: Final = f"{self.file}:{self.line}" if self.file is not None and self.line is not None else ""
|
|
qualified: Final = f"{self.module_path}::{self.function}" if self.module_path is not None else self.function
|
|
return f"{location} {qualified}" if location else qualified
|
|
|
|
|
|
class PythonProfiler:
|
|
def __init__(self, source_root: Path) -> None:
|
|
self._source_root: Final = str(source_root.resolve()) + "/"
|
|
self._seen_frames: Final[set[FrameType]] = set()
|
|
self._event_ids: Final[dict[FrameType, int]] = {}
|
|
self.events: Final[list[FunctionTraceEvent]] = []
|
|
|
|
def __call__(self, frame: FrameType, event: str, _arg: object) -> None:
|
|
if event != "call" or frame in self._seen_frames:
|
|
return
|
|
function_name: Final = self.function_name(frame)
|
|
if function_name is None:
|
|
return
|
|
event_id: Final = len(self.events)
|
|
parent_id: Final = next(
|
|
(self._event_ids[ancestor] for ancestor in _frame_ancestors(frame) if ancestor in self._event_ids),
|
|
None,
|
|
)
|
|
self._seen_frames.add(frame)
|
|
self._event_ids[frame] = event_id
|
|
self.events.append(FunctionTraceEvent(id=event_id, parent_id=parent_id, function=function_name))
|
|
|
|
def function_name(self, frame: FrameType) -> str | None:
|
|
code: Final = frame.f_code
|
|
if not code.co_filename.startswith(self._source_root):
|
|
return None
|
|
relative: Final = code.co_filename.removeprefix(self._source_root)
|
|
return f"{relative}:{code.co_firstlineno} {_qualified_name(frame)}"
|
|
|
|
|
|
class PythonFunctionUsageProfiler:
|
|
def __init__(self, source_root: Path, functions: frozenset[str]) -> None:
|
|
self._source_root: Final = str(source_root.resolve()) + "/"
|
|
self._functions: Final = functions
|
|
self.called: Final[set[str]] = set()
|
|
|
|
def __call__(self, frame: FrameType, event: str, _arg: object) -> None:
|
|
if event != "call":
|
|
return
|
|
code: Final = frame.f_code
|
|
if not code.co_filename.startswith(self._source_root):
|
|
return
|
|
relative: Final = code.co_filename.removeprefix(self._source_root)
|
|
function: Final = f"{relative}:{code.co_firstlineno} {_qualified_name(frame)}"
|
|
if function in self._functions:
|
|
self.called.add(function)
|
|
|
|
|
|
def _qualified_name(frame: FrameType) -> str:
|
|
code: Final = frame.f_code
|
|
native: Final = getattr(code, "co_qualname", None)
|
|
if isinstance(native, str):
|
|
return native
|
|
enclosing: Final = next(
|
|
(
|
|
name
|
|
for ancestor in _frame_ancestors(frame)
|
|
for declared_code, name in _declared_functions(ancestor.f_locals, frozenset())
|
|
if declared_code is code
|
|
),
|
|
None,
|
|
)
|
|
if enclosing is not None:
|
|
return enclosing
|
|
module_name: Final = frame.f_globals.get("__name__")
|
|
if not isinstance(module_name, str):
|
|
return code.co_name
|
|
return _module_qualnames(module_name).get(code, code.co_name)
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def _module_qualnames(module_name: str) -> Mapping[CodeType, str]:
|
|
module: Final = sys.modules.get(module_name)
|
|
if module is None:
|
|
return MappingProxyType({})
|
|
return MappingProxyType(dict(_declared_functions(vars(module), frozenset())))
|
|
|
|
|
|
def _declared_functions(namespace: Mapping[str, object], visited: frozenset[int]) -> Iterator[tuple[CodeType, str]]:
|
|
for attribute in tuple(namespace.values()):
|
|
for value in _accessors(attribute):
|
|
if isinstance(value, FunctionType):
|
|
yield from ((wrapped.__code__, wrapped.__qualname__) for wrapped in _unwrapped(value))
|
|
elif isinstance(value, type) and id(value) not in visited:
|
|
yield from _declared_functions(dict(vars(value)), visited | {id(value)})
|
|
|
|
|
|
def _unwrapped(function: FunctionType) -> Iterator[FunctionType]:
|
|
yield function
|
|
inner: Final = getattr(function, "__wrapped__", None)
|
|
if isinstance(inner, FunctionType):
|
|
yield from _unwrapped(inner)
|
|
|
|
|
|
def _accessors(value: object) -> tuple[object, ...]:
|
|
if isinstance(value, (staticmethod, classmethod)):
|
|
return (value.__func__,)
|
|
if isinstance(value, property):
|
|
return tuple(accessor for accessor in (value.fget, value.fset, value.fdel) if accessor is not None)
|
|
return (value,)
|
|
|
|
|
|
def _frame_ancestors(frame: FrameType) -> Generator[FrameType]:
|
|
ancestor: Final = frame.f_back
|
|
if ancestor is not None:
|
|
yield ancestor
|
|
yield from _frame_ancestors(ancestor)
|
|
|
|
|
|
@contextmanager
|
|
def profile_python(source_root: Path, *, threads: bool = False) -> Generator[PythonProfiler]:
|
|
profiler: Final = PythonProfiler(source_root)
|
|
previous_thread: Final = threading.getprofile()
|
|
if threads:
|
|
threading.setprofile(profiler)
|
|
previous: Final = sys.getprofile()
|
|
sys.setprofile(profiler)
|
|
try:
|
|
yield profiler
|
|
finally:
|
|
sys.setprofile(previous)
|
|
if threads:
|
|
threading.setprofile(previous_thread)
|
|
|
|
|
|
@contextmanager
|
|
def profile_python_function_usage(
|
|
source_root: Path,
|
|
functions: frozenset[str],
|
|
*,
|
|
threads: bool = False,
|
|
) -> Generator[PythonFunctionUsageProfiler]:
|
|
profiler: Final = PythonFunctionUsageProfiler(source_root, functions)
|
|
previous_thread: Final = threading.getprofile()
|
|
if threads:
|
|
threading.setprofile(profiler)
|
|
previous: Final = sys.getprofile()
|
|
sys.setprofile(profiler)
|
|
try:
|
|
yield profiler
|
|
finally:
|
|
sys.setprofile(previous)
|
|
if threads:
|
|
threading.setprofile(previous_thread)
|