Merge pull request #478 from goodboy/wkt/boot_latency_470

Trim `import tractor` 0.42s -> 0.15s (gh #470)
wkt/ai_skillz_run_tests_landing
Bd 2026-08-13 15:02:13 -04:00 committed by GitHub
commit 7554f90e59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 600 additions and 44 deletions

View File

@ -0,0 +1,83 @@
---
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

@ -0,0 +1,122 @@
---
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

@ -23,18 +23,10 @@ async def endpoint(
await trio.sleep_forever() await trio.sleep_forever()
async def spawn_and_open_ep( async def open_ep(
an: tractor.ActorNursery, ptl: tractor.Portal,
i: int, i: int,
) -> None: ) -> None:
'''
Spawn a subactor, start a remote `endpoint()`-task in it.
'''
ptl: tractor.Portal = await an.start_actor(
name=f'worker_{i}',
enable_modules=[__name__],
)
ctx: tractor.Context ctx: tractor.Context
async with ptl.open_context(endpoint) as ( async with ptl.open_context(endpoint) as (
ctx, ctx,
@ -47,7 +39,33 @@ async def spawn_and_open_ep(
await ctx.wait_for_result() await ctx.wait_for_result()
async def main(): async def spawn_and_open_ep(
an: tractor.ActorNursery,
i: int,
maybe_ptl: tractor.Portal|None = None,
) -> None:
'''
Spawn a subactor, start a remote `endpoint()`-task in it.
'''
if maybe_ptl is None:
maybe_ptl: tractor.Portal = await an.start_actor(
name=f'worker_{i}',
enable_modules=[__name__],
)
await open_ep(
ptl=maybe_ptl,
i=i,
)
async def main(
# spawn subs concurrently (in bg `trio.Task`s) so each
# actor's cold `import tractor` (~0.4s, see #470) overlaps
# instead of stacking; once forkserver (#463) lands, spawn
# is cheap enough to just loop sequentially.
spawn_subs_in_bg_tasks: bool = True,
):
''' '''
Spawn a subactor-per-CPU then self-destruct the cluster. Spawn a subactor-per-CPU then self-destruct the cluster.
@ -60,17 +78,21 @@ async def main():
# https://github.com/goodboy/tractor/pull/463 # https://github.com/goodboy/tractor/pull/463
# start_method='main_thread_forkserver', # start_method='main_thread_forkserver',
) as an, ) as an,
# spawn subs concurrently (in bg `trio.Task`s) so each
# actor's cold `import tractor` (~0.4s, see #470) overlaps
# instead of stacking; once forkserver (#463) lands, spawn
# is cheap enough to just loop sequentially.
trio.open_nursery() as tn, trio.open_nursery() as tn,
): ):
for i in range(cpu_count()): for i in range(cpu_count()):
maybe_ptl: tractor.Portal|None = None
if not spawn_subs_in_bg_tasks:
maybe_ptl: tractor.Portal = await an.start_actor(
name=f'worker_{i}',
enable_modules=[__name__],
)
tn.start_soon( tn.start_soon(
spawn_and_open_ep, spawn_and_open_ep,
an, an,
i, i,
maybe_ptl,
) )
destruct_in: int = 2 destruct_in: int = 2
print( print(

View File

@ -0,0 +1,160 @@
'''
Regression tests for the cold package import surface.
'''
import json
import os
from statistics import median
import subprocess
import sys
from typing import (
Any,
get_type_hints,
)
from tractor.discovery import (
_addr,
_multiaddr,
)
from tractor.ipc import (
_tcp,
_uds,
)
def run_cold_import(code: str) -> dict[str, object]:
result = subprocess.run(
[
sys.executable,
'-c',
code,
],
check=True,
capture_output=True,
text=True,
)
return json.loads(result.stdout)
def test_lazy_to_asyncio_package_api():
'''
Keep the public lazy submodule discoverable without eagerly
importing it.
Before the lazy conversion, package import side effects exposed
`to_asyncio` to `dir()` and wildcard imports. Exercise those APIs
in cold interpreters so this test proves normal `import tractor`
leaves `asyncio` unloaded, while discovery and wildcard access
still advertise and resolve the public submodule.
'''
cold = run_cold_import(
'import json, sys, tractor; '
'print(json.dumps({'
'"advertised": "to_asyncio" in dir(tractor), '
'"asyncio_loaded": "asyncio" in sys.modules}))'
)
assert cold == {
'advertised': True,
'asyncio_loaded': False,
}
wildcard = run_cold_import(
'import json; '
'from tractor import *; '
'print(json.dumps({'
'"module": to_asyncio.__name__}))'
)
assert wildcard == {
'module': 'tractor.to_asyncio',
}
def test_cold_import_budget():
'''
Keep cold package import below the pre-optimization regression.
The original `inspect.stack()` caller lookup made a fresh
`import tractor` take about 0.42s and dominate actor startup.
Run seven independent interpreters and gate their median at a
deliberately broad 0.35s: over twice the measured ~0.145s
baseline, but low enough to catch restoration of that hot path.
Taking the median absorbs process-start and shared-runner noise.
The child measures only its import, rather than parent-side
process creation. `TRACTOR_IMPORT_BUDGET_S` provides an explicit,
reviewable override for platforms that establish a different
baseline instead of silently weakening the project default.
Each child also reports the modules whose eager loading this PR
intentionally removes, proving a timing pass cannot hide a
dependency-import regression.
'''
budget_s = float(
os.environ.get(
'TRACTOR_IMPORT_BUDGET_S',
'0.35',
)
)
optional_mods = (
'asyncio',
'bidict',
'colorlog',
'multiaddr',
'wrapt',
)
code = (
'import json, sys, time; '
'started = time.perf_counter(); '
'import tractor; '
'elapsed = time.perf_counter() - started; '
f'optional = {optional_mods!r}; '
'print(json.dumps({'
'"elapsed": elapsed, '
'"loaded": [name for name in optional '
'if name in sys.modules]}))'
)
samples = [
run_cold_import(code)
for _ in range(7)
]
elapsed = [
float(sample['elapsed'])
for sample in samples
]
loaded = {
name
for sample in samples
for name in sample['loaded']
}
assert not loaded
assert median(elapsed) < budget_s, (
f'cold import median exceeded {budget_s:.3f}s budget: '
f'{elapsed!r}'
)
def test_lazy_annotation_names_resolve():
'''
Resolve annotations without importing optional dependencies.
Moving annotation-only third-party names under `TYPE_CHECKING`
left their runtime globals undefined, causing
`typing.get_type_hints()` to raise `NameError`. Resolve every
affected API and prove the lazy aliases retain import-free runtime
introspection.
'''
assert get_type_hints(_multiaddr.mk_maddr)['return'] is Any
assert get_type_hints(_tcp.MsgpackTCPStream.maddr.fget)[
'return'
] is Any
assert get_type_hints(_uds.MsgpackUDSStream.maddr.fget)[
'return'
] == Any|str
assert get_type_hints(_addr.Address.get_random)[
'current_actor'
] is Any
assert _addr.__annotations__['_address_types'].startswith('dict')

View File

@ -2,9 +2,11 @@
`tractor.log`-wrapping unit tests. `tractor.log`-wrapping unit tests.
''' '''
import importlib
import logging import logging
from pathlib import Path from pathlib import Path
import shutil import shutil
import sys
from types import ModuleType from types import ModuleType
import pytest import pytest
@ -165,6 +167,53 @@ def test_implicit_mod_name_applied_for_child(
assert submod.log.logger in sub_logs assert submod.log.logger in sub_logs
def test_implicit_mod_name_from_unregistered_namespace(
tmp_path: Path,
):
'''
Preserve implicit logger naming for dynamic module namespaces.
The fast `sys.modules` caller lookup cannot resolve `runpy`,
plugin-loader, or `exec()` namespaces that are not registered.
Compile a real package file under an unregistered module name so
the rare filename fallback must recover its imported package and
retain the same package-level logger name.
'''
pkg_name = 'dynamic_logger_pkg'
pkg_dir = tmp_path / pkg_name
pkg_dir.mkdir()
init_path = pkg_dir / '__init__.py'
init_path.write_text('')
mod_path = pkg_dir / 'plugin.py'
mod_path.write_text('')
sys.path.insert(0, str(tmp_path))
try:
importlib.import_module(pkg_name)
namespace = {
'__name__': f'{pkg_name}.unregistered',
'__package__': pkg_name,
'tractor': tractor,
}
exec(
compile(
'log = tractor.log.get_logger('
f'pkg_name={pkg_name!r})',
str(mod_path),
'exec',
),
namespace,
)
dynamic_log = namespace.get('log')
finally:
sys.path.remove(str(tmp_path))
sys.modules.pop(pkg_name, None)
assert dynamic_log is not None
assert dynamic_log.name == pkg_name
def test_io_custom_level_registered(): def test_io_custom_level_registered():
''' '''
The `IO`(21) level (registered via `add_log_level()` at The `IO`(21) level (registered via `add_log_level()` at

View File

@ -74,3 +74,39 @@ from .discovery._registry import (
Arbiter as Arbiter, Arbiter as Arbiter,
) )
# from . import hilevel as hilevel # from . import hilevel as hilevel
__all__: tuple[str, ...] = tuple(
name
for name in globals()
if not name.startswith('_')
) + (
'to_asyncio',
)
def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
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,14 +39,15 @@ from typing import (
Type, Type,
) )
import pdbp # NOTE, `pdbp` + `wrapt` are lazy-imported at their
# 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()
@ -257,6 +258,7 @@ 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>)
@ -320,6 +322,8 @@ 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,7 +24,6 @@ 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,
@ -53,7 +52,6 @@ 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 (
@ -85,6 +83,10 @@ 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 (
@ -164,6 +166,7 @@ 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:
@ -946,6 +949,7 @@ 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
@ -1059,6 +1063,9 @@ 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,7 +20,6 @@ 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,
@ -52,7 +51,6 @@ 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
@ -66,6 +64,10 @@ 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 (
@ -910,6 +912,9 @@ 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

@ -16,13 +16,13 @@
from __future__ import annotations from __future__ import annotations
from uuid import uuid4 from uuid import uuid4
from typing import ( from typing import (
Any,
Protocol, Protocol,
ClassVar, ClassVar,
Type, Type,
TYPE_CHECKING, TYPE_CHECKING,
) )
from bidict import bidict
from trio import ( from trio import (
SocketListener, SocketListener,
) )
@ -35,7 +35,11 @@ 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 ..runtime._runtime import Actor from ..runtime._runtime import Actor
else:
Actor = Any
log = get_logger() log = get_logger()
@ -170,7 +174,7 @@ class Address(Protocol):
... ...
_address_types: bidict[str, Type[Address]] = { _address_types: dict[str, Type[Address]] = {
'tcp': TCPAddress, 'tcp': TCPAddress,
'uds': UDSAddress 'uds': UDSAddress
} }

View File

@ -24,14 +24,23 @@ 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 (
Any,
from multiaddr import Multiaddr TYPE_CHECKING,
)
if 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 tractor.discovery._addr import Address from tractor.discovery._addr import Address
else:
Multiaddr = Any
Address = Any
# map from tractor-internal `proto_key` identifiers # map from tractor-internal `proto_key` identifiers
# to the standard multiaddr protocol name strings. # to the standard multiaddr protocol name strings.
@ -56,6 +65,8 @@ 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:
@ -98,6 +109,7 @@ 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

@ -20,7 +20,9 @@ TCP implementation of tractor.ipc._transport.MsgTransport protocol
from __future__ import annotations from __future__ import annotations
import ipaddress import ipaddress
from typing import ( from typing import (
Any,
ClassVar, ClassVar,
TYPE_CHECKING,
) )
# from contextlib import ( # from contextlib import (
# asynccontextmanager as acm, # asynccontextmanager as acm,
@ -33,7 +35,6 @@ 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,6 +43,13 @@ 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
else:
Multiaddr = Any
log = get_logger() log = get_logger()

View File

@ -32,6 +32,7 @@ from socket import (
) )
import struct import struct
from typing import ( from typing import (
Any,
Type, Type,
TYPE_CHECKING, TYPE_CHECKING,
ClassVar, ClassVar,
@ -49,7 +50,6 @@ 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
@ -63,7 +63,13 @@ 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
else:
Multiaddr = Any
Actor = Any
# Platform-specific credential passing constants # Platform-specific credential passing constants

View File

@ -26,11 +26,6 @@ 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 (
@ -38,10 +33,16 @@ from logging import (
Logger, Logger,
StreamHandler, StreamHandler,
) )
from types import ModuleType from types import (
FrameType,
ModuleType,
)
import warnings import warnings
import colorlog # type: ignore # NOTE, `colorlog` is lazy-imported in
# `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?
@ -436,16 +437,40 @@ 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 `tractor.get_logger()`. Attempt to get the module which called
`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).
''' '''
callstack: list[FrameInfo] = stack() try:
caller_fi: FrameInfo = callstack[frames_up] caller_frame: FrameType = sys._getframe(frames_up)
caller_mod: ModuleType = getmodule(caller_fi.frame) except ValueError:
return caller_mod return None
mod_name: str|None = caller_frame.f_globals.get(
'__name__',
)
if mod_name is None:
return None
if caller_mod := sys.modules.get(mod_name):
return caller_mod
# Preserve caller discovery for `runpy`, plugin loaders,
# and `exec()` namespaces not registered in `sys.modules`.
# Import `inspect` only on this rare fallback path.
from inspect import getmodule
return getmodule(caller_frame)
# --- Auto--naming-CASE --- # --- Auto--naming-CASE ---
# ------------------------- # -------------------------
@ -780,6 +805,10 @@ 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,7 +30,6 @@ 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 (
@ -332,6 +331,10 @@ 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,7 +38,11 @@ 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,
@ -96,6 +100,7 @@ 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:
@ -156,6 +161,7 @@ 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: