diff --git a/stdlib/@tests/stubtest_allowlists/darwin-py315.txt b/stdlib/@tests/stubtest_allowlists/darwin-py315.txt index c7a5f5c82d3e..0f5b248ad72b 100644 --- a/stdlib/@tests/stubtest_allowlists/darwin-py315.txt +++ b/stdlib/@tests/stubtest_allowlists/darwin-py315.txt @@ -11,10 +11,17 @@ ctypes.c_longdouble_complex._type_ os.NODEV os.__all__ posix.NODEV +readline.get_pre_input_hook + + +# ============================================================= +# Allowlist entries that cannot or should not be fixed; >= 3.15 +# ============================================================= + +# Internal implementation details of the sampling profiler. profiling.sampling.live_collector profiling.sampling.live_collector.collector profiling.sampling.live_collector.constants profiling.sampling.live_collector.display profiling.sampling.live_collector.trend_tracker profiling.sampling.live_collector.widgets -readline.get_pre_input_hook diff --git a/stdlib/@tests/stubtest_allowlists/linux-py315.txt b/stdlib/@tests/stubtest_allowlists/linux-py315.txt index 01f7975aaa97..fe1f0d179656 100644 --- a/stdlib/@tests/stubtest_allowlists/linux-py315.txt +++ b/stdlib/@tests/stubtest_allowlists/linux-py315.txt @@ -59,10 +59,17 @@ posix.STATX_SIZE posix.STATX_TYPE posix.STATX_UID posix.statx +readline.get_pre_input_hook + + +# ============================================================= +# Allowlist entries that cannot or should not be fixed; >= 3.15 +# ============================================================= + +# Internal implementation details of the sampling profiler. profiling.sampling.live_collector profiling.sampling.live_collector.collector profiling.sampling.live_collector.constants profiling.sampling.live_collector.display profiling.sampling.live_collector.trend_tracker profiling.sampling.live_collector.widgets -readline.get_pre_input_hook diff --git a/stdlib/@tests/stubtest_allowlists/py315.txt b/stdlib/@tests/stubtest_allowlists/py315.txt index 37865b7a404b..e78c5dc76381 100644 --- a/stdlib/@tests/stubtest_allowlists/py315.txt +++ b/stdlib/@tests/stubtest_allowlists/py315.txt @@ -173,25 +173,6 @@ posixpath.splitroot pprint.PrettyPrinter.__init__ pprint.pformat pprint.pprint -profiling -profiling.sampling -profiling.sampling.binary_collector -profiling.sampling.binary_reader -profiling.sampling.cli -profiling.sampling.collector -profiling.sampling.constants -profiling.sampling.dump -profiling.sampling.errors -profiling.sampling.gecko_collector -profiling.sampling.heatmap_collector -profiling.sampling.jsonl_collector -profiling.sampling.module_utils -profiling.sampling.opcode_utils -profiling.sampling.pstats_collector -profiling.sampling.sample -profiling.sampling.stack_collector -profiling.sampling.string_table -profiling.tracing site.addsitedir site.addsitepackages site.addusersitepackages @@ -269,3 +250,39 @@ xml.etree.ElementTree.__all__ xml.is_valid_name xml.utils zipimport.zipimporter.load_module + + +# ============================================================= +# Allowlist entries that cannot or should not be fixed; >= 3.15 +# ============================================================= + +# Internal implementation details of the sampling profiler. +profiling.sampling.binary_collector +profiling.sampling.binary_reader +profiling.sampling.cli +profiling.sampling.constants +profiling.sampling.dump +profiling.sampling.errors +profiling.sampling.gecko_collector.CATEGORY_CODE_TYPE +profiling.sampling.gecko_collector.CATEGORY_CPU +profiling.sampling.gecko_collector.CATEGORY_EXCEPTION +profiling.sampling.gecko_collector.CATEGORY_GC +profiling.sampling.gecko_collector.CATEGORY_GIL +profiling.sampling.gecko_collector.CATEGORY_NATIVE +profiling.sampling.gecko_collector.CATEGORY_OPCODES +profiling.sampling.gecko_collector.CATEGORY_OTHER +profiling.sampling.gecko_collector.CATEGORY_PYTHON +profiling.sampling.gecko_collector.DEFAULT_SUBCATEGORY +profiling.sampling.gecko_collector.FRAME_ADDRESS_NONE +profiling.sampling.gecko_collector.FRAME_INLINE_DEPTH_ROOT +profiling.sampling.gecko_collector.GECKO_CATEGORIES +profiling.sampling.gecko_collector.GECKO_FORMAT_VERSION +profiling.sampling.gecko_collector.GECKO_PREPROCESSED_VERSION +profiling.sampling.gecko_collector.PROCESS_TYPE_MAIN +profiling.sampling.gecko_collector.RESOURCE_TYPE_LIBRARY +profiling.sampling.gecko_collector.STACKWALK_DISABLED +profiling.sampling.heatmap_collector.FileStats +profiling.sampling.heatmap_collector.TreeNode +profiling.sampling.module_utils +profiling.sampling.opcode_utils +profiling.sampling.sample diff --git a/stdlib/VERSIONS b/stdlib/VERSIONS index 3b67c00b1509..fccfb3591eff 100644 --- a/stdlib/VERSIONS +++ b/stdlib/VERSIONS @@ -60,6 +60,7 @@ _py_abc: 3.7- _pydecimal: 3.5- _queue: 3.7- _random: 3.0- +_remote_debugging: 3.15- _sitebuiltins: 3.4- _socket: 3.0- # present in 3.0 at runtime, but not in typeshed _sqlite3: 3.0- @@ -243,6 +244,9 @@ posix: 3.0- posixpath: 3.0- pprint: 3.0- profile: 3.0- +profiling: 3.15- +profiling.sampling: 3.15- +profiling.tracing: 3.15- pstats: 3.0- pty: 3.0- pwd: 3.0- diff --git a/stdlib/_remote_debugging.pyi b/stdlib/_remote_debugging.pyi new file mode 100644 index 000000000000..b001962aad7b --- /dev/null +++ b/stdlib/_remote_debugging.pyi @@ -0,0 +1,183 @@ +from _typeshed import StrOrBytesPath, structseq +from collections.abc import Callable +from typing import Final, TypeAlias, final +from typing_extensions import Self + +_Location: TypeAlias = tuple[int, int, int, int] | LocationInfo | None +_Frame: TypeAlias = tuple[str, _Location, str, int | None] | FrameInfo +_Stats: TypeAlias = dict[str, int | float] + +PROCESS_VM_READV_SUPPORTED: Final[int] +THREAD_STATUS_GIL_REQUESTED: Final[int] +THREAD_STATUS_HAS_EXCEPTION: Final[int] +THREAD_STATUS_HAS_GIL: Final[int] +THREAD_STATUS_MAIN_THREAD: Final[int] +THREAD_STATUS_ON_CPU: Final[int] +THREAD_STATUS_UNKNOWN: Final[int] + +@final +class LocationInfo(structseq[int], tuple[int, int, int, int]): + __match_args__: Final = ("lineno", "end_lineno", "col_offset", "end_col_offset") + @property + def lineno(self) -> int: ... + @property + def end_lineno(self) -> int: ... + @property + def col_offset(self) -> int: ... + @property + def end_col_offset(self) -> int: ... + +@final +class FrameInfo(structseq[object], tuple[str, _Location, str, int | None]): + __match_args__: Final = ("filename", "location", "funcname", "opcode") + @property + def filename(self) -> str: ... + @property + def location(self) -> _Location: ... + @property + def funcname(self) -> str: ... + @property + def opcode(self) -> int | None: ... + +@final +class CoroInfo(structseq[object], tuple[list[_Frame], int | str]): + __match_args__: Final = ("call_stack", "task_name") + @property + def call_stack(self) -> list[_Frame]: ... + @property + def task_name(self) -> int | str: ... + +@final +class TaskInfo(structseq[object], tuple[int, str, list[CoroInfo], list[CoroInfo]]): + __match_args__: Final = ("task_id", "task_name", "coroutine_stack", "awaited_by") + @property + def task_id(self) -> int: ... + @property + def task_name(self) -> str: ... + @property + def coroutine_stack(self) -> list[CoroInfo]: ... + @property + def awaited_by(self) -> list[CoroInfo]: ... + +@final +class ThreadInfo(structseq[object], tuple[int, int, list[_Frame]]): + __match_args__: Final = ("thread_id", "status", "frame_info") + @property + def thread_id(self) -> int: ... + @property + def status(self) -> int: ... + @property + def frame_info(self) -> list[_Frame]: ... + +@final +class InterpreterInfo(structseq[object], tuple[int, list[ThreadInfo]]): + __match_args__: Final = ("interpreter_id", "threads") + @property + def interpreter_id(self) -> int: ... + @property + def threads(self) -> list[ThreadInfo]: ... + +@final +class AwaitedInfo(structseq[object], tuple[int, list[TaskInfo]]): + __match_args__: Final = ("thread_id", "awaited_by") + @property + def thread_id(self) -> int: ... + @property + def awaited_by(self) -> list[TaskInfo]: ... + +@final +class GCStatsInfo(structseq[object], tuple[int, int, int, int, int, int, int, int, int, float]): + __match_args__: Final = ( + "gen", + "iid", + "ts_start", + "ts_stop", + "collections", + "collected", + "uncollectable", + "candidates", + "heap_size", + "duration", + ) + @property + def gen(self) -> int: ... + @property + def iid(self) -> int: ... + @property + def ts_start(self) -> int: ... + @property + def ts_stop(self) -> int: ... + @property + def collections(self) -> int: ... + @property + def collected(self) -> int: ... + @property + def uncollectable(self) -> int: ... + @property + def candidates(self) -> int: ... + @property + def heap_size(self) -> int: ... + @property + def duration(self) -> float: ... + +@final +class RemoteUnwinder: + def __init__( + self, + pid: int, + *, + all_threads: bool = False, + only_active_thread: bool = False, + mode: int = 0, + debug: bool = False, + skip_non_matching_threads: bool = True, + native: bool = False, + gc: bool = False, + opcodes: bool = False, + cache_frames: bool = False, + stats: bool = False, + ) -> None: ... + def get_stack_trace(self) -> list[InterpreterInfo]: ... + def get_all_awaited_by(self) -> list[AwaitedInfo]: ... + def get_async_stack_trace(self) -> list[AwaitedInfo]: ... + def get_stats(self) -> _Stats: ... + def pause_threads(self) -> bool: ... + def resume_threads(self) -> bool: ... + +@final +class GCMonitor: + def __init__(self, pid: int, *, debug: bool = False) -> None: ... + def get_gc_stats(self, all_interpreters: bool = False) -> list[GCStatsInfo]: ... + +@final +class BinaryWriter: + def __init__( + self, filename: StrOrBytesPath, sample_interval_us: int, start_time_us: int, *, compression: int = 0 + ) -> None: ... + @property + def total_samples(self) -> int: ... + def write_sample(self, stack_frames: list[InterpreterInfo], timestamp_us: int) -> None: ... + def finalize(self) -> None: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type: object = None, exc_val: object = None, exc_tb: object = None) -> bool: ... + def get_stats(self) -> _Stats: ... + +@final +class BinaryReader: + def __init__(self, filename: StrOrBytesPath) -> None: ... + @property + def sample_count(self) -> int: ... + @property + def sample_interval_us(self) -> int: ... + def replay(self, collector: object, progress_callback: Callable[[int, int], object] | None = None) -> int: ... + def get_info(self) -> dict[str, object]: ... + def get_stats(self) -> _Stats: ... + def close(self) -> None: ... + def __enter__(self) -> Self: ... + def __exit__(self, exc_type: object = None, exc_val: object = None, exc_tb: object = None) -> bool: ... + +def zstd_available() -> bool: ... +def get_child_pids(pid: int, *, recursive: bool = True) -> list[int]: ... +def is_python_process(pid: int) -> bool: ... +def get_gc_stats(pid: int, *, all_interpreters: bool = False) -> list[GCStatsInfo]: ... diff --git a/stdlib/profiling/__init__.pyi b/stdlib/profiling/__init__.pyi new file mode 100644 index 000000000000..435f5a6cc66c --- /dev/null +++ b/stdlib/profiling/__init__.pyi @@ -0,0 +1,3 @@ +from . import sampling as sampling, tracing as tracing + +__all__ = ("tracing", "sampling") diff --git a/stdlib/profiling/sampling/__init__.pyi b/stdlib/profiling/sampling/__init__.pyi new file mode 100644 index 000000000000..1f8b3f7d98fe --- /dev/null +++ b/stdlib/profiling/sampling/__init__.pyi @@ -0,0 +1,17 @@ +from .collector import Collector as Collector +from .gecko_collector import GeckoCollector as GeckoCollector +from .heatmap_collector import HeatmapCollector as HeatmapCollector +from .jsonl_collector import JsonlCollector as JsonlCollector +from .pstats_collector import PstatsCollector as PstatsCollector +from .stack_collector import CollapsedStackCollector as CollapsedStackCollector +from .string_table import StringTable as StringTable + +__all__ = ( + "Collector", + "PstatsCollector", + "CollapsedStackCollector", + "HeatmapCollector", + "GeckoCollector", + "JsonlCollector", + "StringTable", +) diff --git a/stdlib/profiling/sampling/collector.pyi b/stdlib/profiling/sampling/collector.pyi new file mode 100644 index 000000000000..f191dbb66039 --- /dev/null +++ b/stdlib/profiling/sampling/collector.pyi @@ -0,0 +1,24 @@ +from _typeshed import StrOrBytesPath +from abc import ABC, abstractmethod +from collections.abc import Sequence +from typing import TypeAlias + +from _remote_debugging import AwaitedInfo, FrameInfo, InterpreterInfo, LocationInfo + +_Location: TypeAlias = int | tuple[int, int, int, int] | LocationInfo | None +_Frame: TypeAlias = FrameInfo | tuple[str, _Location, str, int | None] +_Timestamps: TypeAlias = Sequence[int] | None + +def normalize_location(location: _Location) -> tuple[int, int, int, int]: ... +def extract_lineno(location: _Location) -> int: ... +def filter_internal_frames(frames: Sequence[_Frame]) -> list[_Frame]: ... +def iter_async_frames(awaited_info_list: Sequence[AwaitedInfo]) -> object: ... + +class Collector(ABC): + @abstractmethod + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def collect_failed_sample(self) -> None: ... + @abstractmethod + def export(self, filename: StrOrBytesPath) -> None: ... diff --git a/stdlib/profiling/sampling/gecko_collector.pyi b/stdlib/profiling/sampling/gecko_collector.pyi new file mode 100644 index 000000000000..6072d4f2359a --- /dev/null +++ b/stdlib/profiling/sampling/gecko_collector.pyi @@ -0,0 +1,13 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Timestamps + +class GeckoCollector(Collector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False, opcodes: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... diff --git a/stdlib/profiling/sampling/heatmap_collector.pyi b/stdlib/profiling/sampling/heatmap_collector.pyi new file mode 100644 index 000000000000..bd523bc38245 --- /dev/null +++ b/stdlib/profiling/sampling/heatmap_collector.pyi @@ -0,0 +1,24 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Frame, _Timestamps + +class HeatmapCollector(Collector): + FILE_INDEX_FORMAT: str + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, output_path: StrOrBytesPath) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + def set_stats( + self, + sample_interval_usec: int, + duration_sec: float, + sample_rate: float, + error_rate: float | None = None, + missed_samples: float | None = None, + **kwargs: object, + ) -> None: ... diff --git a/stdlib/profiling/sampling/jsonl_collector.pyi b/stdlib/profiling/sampling/jsonl_collector.pyi new file mode 100644 index 000000000000..3bdc4b81c01d --- /dev/null +++ b/stdlib/profiling/sampling/jsonl_collector.pyi @@ -0,0 +1,15 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import _Frame, _Timestamps +from .stack_collector import StackTraceCollector + +class JsonlCollector(StackTraceCollector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False, mode: int | None = None) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + def process_frames(self, frames: Sequence[_Frame], _thread_id: int, weight: int = 1) -> None: ... diff --git a/stdlib/profiling/sampling/pstats_collector.pyi b/stdlib/profiling/sampling/pstats_collector.pyi new file mode 100644 index 000000000000..178d55a7af8e --- /dev/null +++ b/stdlib/profiling/sampling/pstats_collector.pyi @@ -0,0 +1,17 @@ +from _typeshed import StrOrBytesPath +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Timestamps + +class PstatsCollector(Collector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + def create_stats(self) -> None: ... + def print_stats( + self, sort: int = -1, limit: int | None = None, show_summary: bool = True, mode: int | None = None + ) -> None: ... diff --git a/stdlib/profiling/sampling/stack_collector.pyi b/stdlib/profiling/sampling/stack_collector.pyi new file mode 100644 index 000000000000..0788e08295ad --- /dev/null +++ b/stdlib/profiling/sampling/stack_collector.pyi @@ -0,0 +1,39 @@ +from _typeshed import StrOrBytesPath +from abc import ABCMeta +from collections.abc import Sequence + +from _remote_debugging import AwaitedInfo, InterpreterInfo + +from .collector import Collector, _Frame, _Timestamps + +class StackTraceCollector(Collector, metaclass=ABCMeta): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + +class CollapsedStackCollector(StackTraceCollector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + +class FlamegraphCollector(StackTraceCollector): + def __init__(self, sample_interval_usec: int, *, skip_idle: bool = False) -> None: ... + def collect( + self, stack_frames: Sequence[InterpreterInfo] | Sequence[AwaitedInfo], timestamps_us: _Timestamps = None + ) -> None: ... + def set_stats( + self, + sample_interval_usec: int, + duration_sec: float, + sample_rate: float, + error_rate: float | None = None, + missed_samples: float | None = None, + mode: int | None = None, + ) -> None: ... + def export(self, filename: StrOrBytesPath) -> None: ... + def process_frames(self, frames: Sequence[_Frame], thread_id: int, weight: int = 1) -> None: ... + +class DiffFlamegraphCollector(FlamegraphCollector): + def __init__(self, sample_interval_usec: int, *, baseline_binary_path: StrOrBytesPath, skip_idle: bool = False) -> None: ... diff --git a/stdlib/profiling/sampling/string_table.pyi b/stdlib/profiling/sampling/string_table.pyi new file mode 100644 index 000000000000..cb71e82ec036 --- /dev/null +++ b/stdlib/profiling/sampling/string_table.pyi @@ -0,0 +1,5 @@ +class StringTable: + def intern(self, string: object) -> int: ... + def get_string(self, index: int) -> str: ... + def get_strings(self) -> list[str]: ... + def __len__(self) -> int: ... diff --git a/stdlib/profiling/tracing.pyi b/stdlib/profiling/tracing.pyi new file mode 100644 index 000000000000..af4ab508406a --- /dev/null +++ b/stdlib/profiling/tracing.pyi @@ -0,0 +1,9 @@ +from cProfile import Profile as Profile, run as run, runctx as runctx +from types import CodeType +from typing import TypeAlias + +__all__ = ("run", "runctx", "Profile") + +_Label: TypeAlias = tuple[str, int, str] + +def label(code: str | CodeType) -> _Label: ... # undocumented