Compare commits

..

No commits in common. "7f98a5801e369ff8559be1725b7a54af3a4ff849" and "65bf9df5bad583bb30e09b2659781370268a6b64" have entirely different histories.

13 changed files with 27 additions and 317 deletions

View File

@ -1,83 +0,0 @@
---
model: claude-fable-5
service: claude
session: b6b42e23-0454-4d9b-be1c-0a89d65aed1b
timestamp: 2026-07-02T15:56:26Z
git_ref: 65bf9df5
scope: code
substantive: true
raw_file: 20260702T155626Z_65bf9df5_prompt_io.raw.md
---
## Prompt
From `ai/prompt-io/prompts/issue_470.md`:
> attempt to resolve
> https://github.com/goodboy/tractor/issues/470
> do it with /open-wkt, ensure you /run-tests on all
> changes
(plus a hard pause-for-human-review deadline of
12:50PM EST.)
Issue #470 asks to trim the ~0.42s `import tractor`
cost — which dominates per-actor spawn latency on the
`trio` backend — by lazy-importing heavy/optional deps
(`pdbp`, `stackscope`, `multiaddr`, + audit of
`colorlog`/`bidict`/`wrapt`/`setproctitle`).
## Response summary
Profiling showed the issue's dep-list only accounted
for ~20ms; the dominant cost (~244ms) was
`log.get_logger()`'s `get_caller_mod()` calling
`inspect.stack()` at module level in ~39 modules —
each call walks every stack frame (deep during nested
imports) and scans `sys.modules` per frame via
`inspect.getmodule()`.
Changes, in impact order:
1. `get_caller_mod()` -> `sys._getframe()` +
`f_globals['__name__']` `sys.modules` lookup
(~240ms saved).
2. Issue's lazy-import checklist: `bidict`,
`multiaddr`, `colorlog`, `wrapt` moved to
`TYPE_CHECKING`/function-local imports;
`platformdirs` function-local; `asyncio` +
`.to_asyncio` deferred out of the `devx.debug` +
`spawn._entry` eager paths (~15ms saved).
3. PEP 562 `__getattr__` on `tractor/__init__.py`
preserving public `tractor.to_asyncio` attr access.
Results: `import tractor` 0.42s -> ~0.145s (~65%);
sequential `start_actor` latency 0.40-0.44s ->
~0.179s/actor. `pdbp` (needs `_repl.py` class-base
restructure) + `platformdirs` (needs
`UDSAddress.def_bindspace` protocol rework) documented
as follow-ups.
## Files changed
- `tractor/log.py``get_caller_mod()` perf fix +
lazy `colorlog`
- `tractor/__init__.py` — PEP 562 lazy `to_asyncio`
- `tractor/discovery/_addr.py``bidict` ->
`TYPE_CHECKING`
- `tractor/discovery/_multiaddr.py` — lazy `multiaddr`
- `tractor/ipc/_tcp.py`, `tractor/ipc/_uds.py`
`Multiaddr` -> `TYPE_CHECKING`
- `tractor/runtime/_state.py` — lazy `platformdirs`
- `tractor/devx/_frame_stack.py` — lazy `pdbp` +
`wrapt`
- `tractor/devx/debug/_trace.py`,
`tractor/devx/debug/_tty_lock.py` — lazy `asyncio` +
`.to_asyncio`
- `tractor/spawn/_entry.py` — lazy
`run_as_asyncio_guest`
## Human edits
None yet — pending user review at the 12:50PM EST
pause gate (test-suite results reported in-session).

View File

@ -1,122 +0,0 @@
---
model: claude-fable-5
service: claude
timestamp: 2026-07-02T15:56:26Z
git_ref: 65bf9df5
diff_cmd: git diff main..wkt/boot_latency_470
---
# Raw AI output — gh #470 `import tractor` latency trim
All generated code is committed on the
`wkt/boot_latency_470` branch; per diff-ref mode each
file's content is referenced via its diff instead of
copied verbatim.
## Profiling findings (verbatim analysis output)
Baseline: `import tractor` ~0.39-0.42s wall.
`python -X importtime` + `cProfile` traced the cost NOT
primarily to third-party deps (the issue's hypothesis)
but to `tractor/log.py:get_logger()` calling
`get_caller_mod()` -> `inspect.stack()` at module level
in ~39 tractor modules:
- `inspect.stack()` builds `FrameInfo` (incl. src-file
and line-context resolution) for EVERY frame on the
stack; during nested imports the stack is dozens of
importlib frames deep.
- each `FrameInfo` resolution calls
`inspect.getmodule()` which scans all of
`sys.modules` per frame (1.4M `ismodule()` calls in
one profiled import).
- aggregate: ~244ms of tractor-own module "self" time
vs ~20ms for ALL the issue-listed third-party deps
(`pdbp` ~10ms, `bidict` ~4.5ms, `multiaddr` ~3.5ms,
`wrapt`/`colorlog` ~1ms each); `trio` itself is
~70-100ms and unavoidable.
## Generated changes
> `git diff main..wkt/boot_latency_470 -- tractor/log.py`
`get_caller_mod()` rewritten from `inspect.stack()` +
`inspect.getmodule()` to `sys._getframe(frames_up)` +
`frame.f_globals['__name__']` -> `sys.modules` lookup
(O(1) vs O(stack x sys.modules)). Unused `inspect`
imports dropped; `FrameType` imported from `types`.
Also `colorlog` lazy-imported inside
`get_console_log()`.
> `git diff main..wkt/boot_latency_470 -- tractor/discovery/_addr.py`
`bidict` import moved under `TYPE_CHECKING`
(annotation-only use; `_address_types` is a plain dict
literal).
> `git diff main..wkt/boot_latency_470 -- tractor/discovery/_multiaddr.py`
`from __future__ import annotations` added; `multiaddr`
import moved under `TYPE_CHECKING` + function-local
imports in `mk_maddr()`/`parse_maddr()`.
> `git diff main..wkt/boot_latency_470 -- tractor/ipc/_tcp.py tractor/ipc/_uds.py`
`Multiaddr` imports moved under `TYPE_CHECKING`
(annotation-only in both transports).
> `git diff main..wkt/boot_latency_470 -- tractor/runtime/_state.py`
`platformdirs` lazy-imported inside `get_rt_dir()`
(NOTE: still imported eagerly via
`UDSAddress.def_bindspace` class-var eval; see
follow-ups).
> `git diff main..wkt/boot_latency_470 -- tractor/devx/_frame_stack.py`
`pdbp` + `wrapt` lazy-imported inside
`hide_runtime_frames()` / `api_frame()` respectively.
> `git diff main..wkt/boot_latency_470 -- tractor/devx/debug/_trace.py tractor/devx/debug/_tty_lock.py`
`asyncio` moved to `TYPE_CHECKING` + call-site local
imports (`asyncio.current_task()` sites);
`tractor.to_asyncio.run_trio_task_in_future` imports
moved into the infected-aio runtime branches.
> `git diff main..wkt/boot_latency_470 -- tractor/spawn/_entry.py`
`run_as_asyncio_guest` import moved into the
`infect_asyncio=True` branches of `_mp_main()` /
`_trio_main()`.
> `git diff main..wkt/boot_latency_470 -- tractor/__init__.py`
PEP 562 module `__getattr__` added so
`tractor.to_asyncio` attr-access still works (required
by `tests/test_child_manages_service_nursery.py` and
any downstream user) while keeping `asyncio` off the
eager import path.
## Measured results (verbatim)
- `import tractor`: 0.39-0.42s -> ~0.145s (~65% cut)
- `start_actor` spawn+boot+reg+cancel: ~0.40-0.44s ->
~0.179s/actor (n=5 sequential, warm parent)
- post-change eager-module check: only `pdbp` +
`platformdirs` of the issue's list remain eager.
## Known follow-ups (not implemented, deadline-bound)
- `pdbp` (~10ms): still eager via
`devx/debug/_repl.py` class bases
(`class PdbREPL(pdbp.Pdb)`) + `_tty_lock.py`
module-level `@pdbp.hideframe`; needs `_repl`
restructure + PEP 562 in `devx.debug.__init__`.
- `platformdirs` (~1.5ms): eager via
`UDSAddress.def_bindspace: ClassVar = get_rt_dir()`
class-body call; needs `Address`-protocol rework of
`def_bindspace` to a lazy accessor.
- `stackscope` + `setproctitle`: already lazy/absent —
no change needed.

View File

@ -74,26 +74,3 @@ from .discovery._registry import (
Arbiter as Arbiter, Arbiter as Arbiter,
) )
# from . import hilevel as hilevel # from . import hilevel as hilevel
def __getattr__(name: str):
'''
PEP 562 lazy sub-module loading, presently only for
`.to_asyncio` which (transitively) imports `asyncio`
itself: a non-trivial multi-ms chunk of the eager
`import tractor` cost (gh #470) unneeded by
`trio`-only apps.
Any `tractor.to_asyncio.<attr>` access (or a
`from tractor import to_asyncio`) still works, the
sub-mod is simply imported on first-access instead
of at pkg-import time.
'''
if name == 'to_asyncio':
from importlib import import_module
return import_module('.to_asyncio', __name__)
raise AttributeError(
f'module {__name__!r} has no attribute {name!r}'
)

View File

@ -39,15 +39,14 @@ from typing import (
Type, Type,
) )
# NOTE, `pdbp` + `wrapt` are lazy-imported at their import pdbp
# single use-sites below to keep them off the eager
# `import tractor` path (gh #470).
from tractor.log import get_logger from tractor.log import get_logger
import trio import trio
from tractor.msg import ( from tractor.msg import (
pretty_struct, pretty_struct,
NamespacePath, NamespacePath,
) )
import wrapt
log = get_logger() log = get_logger()
@ -258,7 +257,6 @@ def api_frame(
caller_frames_up: int = 1, caller_frames_up: int = 1,
) -> Callable: ) -> Callable:
import wrapt
# handle the decorator called WITHOUT () case, # handle the decorator called WITHOUT () case,
# i.e. just @api_frame, NOT @api_frame(extra=<blah>) # i.e. just @api_frame, NOT @api_frame(extra=<blah>)
@ -322,8 +320,6 @@ def hide_runtime_frames() -> dict[FunctionType, CodeType]:
as possible, particularly from inside a `PdbREPL`. as possible, particularly from inside a `PdbREPL`.
''' '''
import pdbp
# XXX HACKZONE XXX # XXX HACKZONE XXX
# hide exit stack frames on nurseries and cancel-scopes! # hide exit stack frames on nurseries and cancel-scopes!
# |_ so avoid seeing it when the `pdbp` REPL is first engaged from # |_ so avoid seeing it when the `pdbp` REPL is first engaged from

View File

@ -24,6 +24,7 @@ mult-process support within a single actor tree.
''' '''
from __future__ import annotations from __future__ import annotations
import asyncio
import bdb import bdb
from contextlib import ( from contextlib import (
AbstractContextManager, AbstractContextManager,
@ -52,6 +53,7 @@ from trio import (
) )
import tractor import tractor
from tractor.log import get_logger from tractor.log import get_logger
from tractor.to_asyncio import run_trio_task_in_future
from tractor._context import Context from tractor._context import Context
from tractor.runtime import _state from tractor.runtime import _state
from tractor._exceptions import ( from tractor._exceptions import (
@ -83,10 +85,6 @@ from ..pformat import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
# NOTE, `asyncio` (and `.to_asyncio`) are
# lazy-imported at their use-sites to keep them off
# the eager `import tractor` path (gh #470).
import asyncio
from trio.lowlevel import Task from trio.lowlevel import Task
from threading import Thread from threading import Thread
from tractor.runtime._runtime import ( from tractor.runtime._runtime import (
@ -166,7 +164,6 @@ async def _pause(
'An `asyncio` task should not be calling this!?' 'An `asyncio` task should not be calling this!?'
) from rte ) from rte
else: else:
import asyncio
task = asyncio.current_task() task = asyncio.current_task()
if debug_func is not None: if debug_func is not None:
@ -949,7 +946,6 @@ def pause_from_sync(
asyncio_task: asyncio.Task|None = None asyncio_task: asyncio.Task|None = None
if is_infected_aio: if is_infected_aio:
import asyncio
asyncio_task = asyncio.current_task() asyncio_task = asyncio.current_task()
# TODO: we could also check for a non-`.to_thread` context # TODO: we could also check for a non-`.to_thread` context
@ -1063,9 +1059,6 @@ def pause_from_sync(
greenback: ModuleType = maybe_import_greenback() greenback: ModuleType = maybe_import_greenback()
if greenback.has_portal(): if greenback.has_portal():
from tractor.to_asyncio import (
run_trio_task_in_future,
)
DebugStatus.shield_sigint() DebugStatus.shield_sigint()
fute: asyncio.Future = run_trio_task_in_future( fute: asyncio.Future = run_trio_task_in_future(
partial( partial(

View File

@ -20,6 +20,7 @@ Root-actor TTY mutex-locking machinery.
''' '''
from __future__ import annotations from __future__ import annotations
import asyncio
from contextlib import ( from contextlib import (
AbstractContextManager, AbstractContextManager,
asynccontextmanager as acm, asynccontextmanager as acm,
@ -51,6 +52,7 @@ from trio import (
TaskStatus, TaskStatus,
) )
import tractor import tractor
from tractor.to_asyncio import run_trio_task_in_future
from tractor.log import get_logger from tractor.log import get_logger
from tractor._context import Context from tractor._context import Context
from tractor.runtime import _state from tractor.runtime import _state
@ -64,10 +66,6 @@ from tractor.runtime._state import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
# NOTE, `asyncio` (and `.to_asyncio`) are
# lazy-imported at their use-sites to keep them off
# the eager `import tractor` path (gh #470).
import asyncio
from trio.lowlevel import Task from trio.lowlevel import Task
from threading import Thread from threading import Thread
from tractor.ipc import ( from tractor.ipc import (
@ -912,9 +910,6 @@ class DebugStatus:
async def _set_repl_release(): async def _set_repl_release():
repl_release.set() repl_release.set()
from tractor.to_asyncio import (
run_trio_task_in_future,
)
fute: asyncio.Future = run_trio_task_in_future( fute: asyncio.Future = run_trio_task_in_future(
_set_repl_release _set_repl_release
) )

View File

@ -22,6 +22,7 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
) )
from bidict import bidict
from trio import ( from trio import (
SocketListener, SocketListener,
) )
@ -34,9 +35,6 @@ from ..ipc._tcp import TCPAddress
from ..ipc._uds import UDSAddress from ..ipc._uds import UDSAddress
if TYPE_CHECKING: if TYPE_CHECKING:
# ONLY type-annots, the eager import costs ~4.5ms
# of `import tractor` wall-time (gh #470).
from bidict import bidict
from ..runtime._runtime import Actor from ..runtime._runtime import Actor
log = get_logger() log = get_logger()

View File

@ -24,16 +24,13 @@ Multiaddress support using the upstream `py-multiaddr` lib
- https://github.com/multiformats/multiaddr/blob/master/protocols/unix.md - https://github.com/multiformats/multiaddr/blob/master/protocols/unix.md
''' '''
from __future__ import annotations
import ipaddress import ipaddress
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING:
# NOTE, `multiaddr` is lazy-imported at first use
# (in the fns below) to keep it off the eager
# `import tractor` path (gh #470).
from multiaddr import Multiaddr from multiaddr import Multiaddr
if TYPE_CHECKING:
from tractor.discovery._addr import Address from tractor.discovery._addr import Address
# map from tractor-internal `proto_key` identifiers # map from tractor-internal `proto_key` identifiers
@ -59,8 +56,6 @@ def mk_maddr(
multiaddr-spec-compliant protocol path. multiaddr-spec-compliant protocol path.
''' '''
from multiaddr import Multiaddr
proto_key: str = addr.proto_key proto_key: str = addr.proto_key
maddr_proto: str|None = _tpt_proto_to_maddr.get(proto_key) maddr_proto: str|None = _tpt_proto_to_maddr.get(proto_key)
if maddr_proto is None: if maddr_proto is None:
@ -103,7 +98,6 @@ def parse_maddr(
''' '''
# lazy imports to avoid circular deps # lazy imports to avoid circular deps
from multiaddr import Multiaddr
from tractor.ipc._tcp import TCPAddress from tractor.ipc._tcp import TCPAddress
from tractor.ipc._uds import UDSAddress from tractor.ipc._uds import UDSAddress

View File

@ -21,7 +21,6 @@ from __future__ import annotations
import ipaddress import ipaddress
from typing import ( from typing import (
ClassVar, ClassVar,
TYPE_CHECKING,
) )
# from contextlib import ( # from contextlib import (
# asynccontextmanager as acm, # asynccontextmanager as acm,
@ -34,6 +33,7 @@ from trio import (
open_tcp_listeners, open_tcp_listeners,
) )
from multiaddr import Multiaddr
from tractor.msg import MsgCodec from tractor.msg import MsgCodec
from tractor.log import get_logger from tractor.log import get_logger
from tractor.discovery._multiaddr import mk_maddr from tractor.discovery._multiaddr import mk_maddr
@ -42,11 +42,6 @@ from tractor.ipc._transport import (
MsgpackTransport, MsgpackTransport,
) )
if TYPE_CHECKING:
# ONLY type-annots, the eager import costs
# `import tractor` wall-time (gh #470).
from multiaddr import Multiaddr
log = get_logger() log = get_logger()

View File

@ -49,6 +49,7 @@ from trio._highlevel_open_unix_stream import (
has_unix, has_unix,
) )
from multiaddr import Multiaddr
from tractor.msg import MsgCodec from tractor.msg import MsgCodec
from tractor.log import get_logger from tractor.log import get_logger
from tractor.discovery._multiaddr import mk_maddr from tractor.discovery._multiaddr import mk_maddr
@ -62,9 +63,6 @@ from tractor.runtime._state import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
# ONLY type-annots, the eager import costs
# `import tractor` wall-time (gh #470).
from multiaddr import Multiaddr
from tractor.runtime._runtime import Actor from tractor.runtime._runtime import Actor

View File

@ -26,6 +26,11 @@ built on `tractor`.
''' '''
from collections.abc import Mapping from collections.abc import Mapping
from functools import partial from functools import partial
from inspect import (
FrameInfo,
getmodule,
stack,
)
import sys import sys
import logging import logging
from logging import ( from logging import (
@ -33,16 +38,10 @@ from logging import (
Logger, Logger,
StreamHandler, StreamHandler,
) )
from types import ( from types import ModuleType
FrameType,
ModuleType,
)
import warnings import warnings
# NOTE, `colorlog` is lazy-imported in import colorlog # type: ignore
# `get_console_log()` to keep it off the eager
# `import tractor` path (gh #470).
#
# ?TODO, some other (modern) alt libs? # ?TODO, some other (modern) alt libs?
# import coloredlogs # import coloredlogs
# import colored_traceback.auto # ?TODO, need better config? # import colored_traceback.auto # ?TODO, need better config?
@ -439,33 +438,16 @@ def get_logger(
pkg_name: str = _root_name pkg_name: str = _root_name
def get_caller_mod( def get_caller_mod(
frames_up: int = 2, frames_up:int = 2
) -> ModuleType|None: ):
''' '''
Attempt to get the module which called Attempt to get the module which called `tractor.get_logger()`.
`tractor.get_logger()`.
Resolve the caller's frame with `sys._getframe()` and
map its `__name__` through `sys.modules`; `inspect.stack()`
(the previous impl) builds src-file info for EVERY frame
on the stack, scanning all of `sys.modules` per frame via
`inspect.getmodule()`, which made module-level
`get_logger()` calls dominate `import tractor` time
(see gh #470).
''' '''
try: callstack: list[FrameInfo] = stack()
caller_frame: FrameType = sys._getframe(frames_up) caller_fi: FrameInfo = callstack[frames_up]
except ValueError: caller_mod: ModuleType = getmodule(caller_fi.frame)
return None return caller_mod
mod_name: str|None = caller_frame.f_globals.get(
'__name__',
)
if mod_name is None:
return None
return sys.modules.get(mod_name)
# --- Auto--naming-CASE --- # --- Auto--naming-CASE ---
# ------------------------- # -------------------------
@ -800,10 +782,6 @@ def get_console_log(
None, None,
) )
): ):
# lazy-imported to keep it off the eager
# `import tractor` path (gh #470).
import colorlog # type: ignore
fmt: str = LOG_FORMAT # always apply our format? fmt: str = LOG_FORMAT # always apply our format?
handler = StreamHandler() handler = StreamHandler()
formatter = colorlog.ColoredFormatter( formatter = colorlog.ColoredFormatter(

View File

@ -30,6 +30,7 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
) )
import platformdirs
from trio.lowlevel import current_task from trio.lowlevel import current_task
from msgspec import ( from msgspec import (
@ -331,10 +332,6 @@ def get_rt_dir(
the lovely `platformdirs` lib. the lovely `platformdirs` lib.
''' '''
# lazy-imported to keep it off the eager
# `import tractor` path (gh #470).
import platformdirs
rt_dir: Path = Path( rt_dir: Path = Path(
platformdirs.user_runtime_dir( platformdirs.user_runtime_dir(
appname=appname, appname=appname,

View File

@ -38,11 +38,7 @@ from ..devx import (
pformat, pformat,
) )
# from ..msg import pretty_struct # from ..msg import pretty_struct
# from ..to_asyncio import run_as_asyncio_guest
# NOTE, `.to_asyncio` (and thus `asyncio` itself) is
# lazy-imported at the `infect_asyncio=True` use-sites
# below to keep it off the eager `import tractor` path
# (gh #470).
from ..discovery._addr import UnwrappedAddress from ..discovery._addr import UnwrappedAddress
from ..runtime._runtime import ( from ..runtime._runtime import (
async_main, async_main,
@ -100,7 +96,6 @@ def _mp_main(
) )
try: try:
if infect_asyncio: if infect_asyncio:
from ..to_asyncio import run_as_asyncio_guest
actor._infected_aio = True actor._infected_aio = True
run_as_asyncio_guest(trio_main) run_as_asyncio_guest(trio_main)
else: else:
@ -161,7 +156,6 @@ def _trio_main(
) )
try: try:
if infect_asyncio: if infect_asyncio:
from ..to_asyncio import run_as_asyncio_guest
actor._infected_aio = True actor._infected_aio = True
run_as_asyncio_guest(trio_main) run_as_asyncio_guest(trio_main)
else: else: