Skip to content

Commit caf43bb

Browse files
committed
added some debug scripts
1 parent 02c9cb4 commit caf43bb

File tree

4 files changed

+171
-15
lines changed

4 files changed

+171
-15
lines changed

debug/src_size.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python
2+
"""
3+
src_size.py <path-or-vcs-url>
4+
================================
5+
Measure how much space a Python package (plus its **runtime** dependencies)
6+
occupies when built **from source**, without counting the extra build tools we
7+
install inside a temporary virtual-env (``pip``, ``setuptools``, ``wheel``,
8+
etc.).
9+
10+
What you get
11+
------------
12+
1. **Raw source** - size of the directory / checkout you point at.
13+
2. **Built artefacts** - combined size of the sdist + wheel produced by
14+
``python -m build``.
15+
3. **Runtime tree** - bytes taken by the package **and its runtime deps** once
16+
installed, *excluding* build-time tooling.
17+
4. *(optional)* a **breakdown** of every runtime distribution, sorted
18+
large → small.
19+
20+
CLI flags
21+
---------
22+
```
23+
--no-deps Skip installing dependencies (handy for library-only size)
24+
--breakdown, -b Show per-package size table
25+
--include-tools Include build tools (pip/setuptools/wheel) in totals + table
26+
```
27+
28+
> **Note**The script still seeds pip inside the venv so it works on
29+
> pip-less interpreters - those files are just ignored by default in the
30+
> final numbers.
31+
"""
32+
33+
from __future__ import annotations
34+
35+
import argparse
36+
import ensurepip
37+
import json
38+
import os
39+
import pathlib
40+
import subprocess
41+
import sys
42+
import tempfile
43+
from typing import List, Tuple
44+
45+
BYTES_IN_MB = 1_048_576
46+
BUILD_TOOLS = {"pip", "setuptools", "wheel"}
47+
48+
# ---------------------------------------------------------------------------
49+
# Helpers
50+
# ---------------------------------------------------------------------------
51+
52+
def du(path: pathlib.Path) -> int:
53+
"""Recursive size of *path* in bytes."""
54+
return sum(p.stat().st_size for p in path.rglob("*") if p.is_file())
55+
56+
57+
def run(cmd: List[str]) -> None:
58+
subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
59+
60+
61+
def build_artefacts(src: pathlib.Path, out: pathlib.Path) -> List[pathlib.Path]:
62+
run([sys.executable, "-m", "pip", "install", "-q", "build"])
63+
run([sys.executable, "-m", "build", "--sdist", "--wheel", "--outdir", out, src])
64+
return list(out.glob("*.*"))
65+
66+
67+
def pick_one(artefacts: List[pathlib.Path]) -> pathlib.Path:
68+
return next((a for a in artefacts if a.suffix == ".whl"), artefacts[0])
69+
70+
71+
def create_venv(venv: pathlib.Path) -> pathlib.Path:
72+
run([sys.executable, "-m", "venv", venv])
73+
py = venv / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
74+
run([str(py), "-m", "ensurepip", "--upgrade"])
75+
run([str(py), "-m", "pip", "install", "-q", "--upgrade", "pip", "setuptools", "wheel"])
76+
return py
77+
78+
79+
def dist_sizes(py: pathlib.Path) -> List[Tuple[str, int]]:
80+
"""Return list of (dist_name, size_bytes) for every distribution in venv."""
81+
code = r'''
82+
import importlib.metadata as m, pathlib, json, os
83+
sizes = {}
84+
for dist in m.distributions():
85+
total = 0
86+
for entry in dist.files or []:
87+
p = pathlib.Path(dist.locate_file(entry))
88+
if p.is_file():
89+
try:
90+
total += p.stat().st_size
91+
except FileNotFoundError:
92+
pass
93+
sizes[dist.metadata['Name']] = total
94+
print(json.dumps(sizes))
95+
'''
96+
out = subprocess.check_output([str(py), "-c", code], text=True)
97+
data = json.loads(out)
98+
return sorted(((k, v) for k, v in data.items()), key=lambda kv: kv[1], reverse=True)
99+
100+
101+
def canonical(name: str) -> str:
102+
return name.lower().replace("_", "-")
103+
104+
105+
# ---------------------------------------------------------------------------
106+
# Main
107+
# ---------------------------------------------------------------------------
108+
109+
def main(src: str, include_deps: bool, show_breakdown: bool, include_tools: bool) -> None:
110+
ensurepip.bootstrap() # ensure pip for outer interpreter
111+
112+
with tempfile.TemporaryDirectory() as tmp_s:
113+
tmp = pathlib.Path(tmp_s)
114+
115+
# 1. Obtain source ---------------------------------------------------
116+
if pathlib.Path(src).is_dir():
117+
src_dir = pathlib.Path(src).resolve()
118+
else:
119+
src_dir = tmp / "clone"
120+
run(["git", "clone", "--depth", "1", src, src_dir])
121+
122+
print(f"Raw source: {du(src_dir)/BYTES_IN_MB:.2f} MB")
123+
124+
# 2. Build artefacts -------------------------------------------------
125+
artefacts = build_artefacts(src_dir, tmp)
126+
print(f"Sdist+wheel: {sum(p.stat().st_size for p in artefacts)/BYTES_IN_MB:.2f} MB")
127+
artefact = pick_one(artefacts)
128+
129+
# 3. Install into temp venv -----------------------------------------
130+
py = create_venv(tmp / "venv")
131+
install_cmd = [str(py), "-m", "pip", "install", "-q", str(artefact)]
132+
if not include_deps:
133+
install_cmd.insert(5, "--no-deps")
134+
run(install_cmd)
135+
136+
dists = dist_sizes(py)
137+
# Filter build tools unless user asked to keep them
138+
runtime_dists = [(n, sz) for n, sz in dists if include_tools or canonical(n) not in {canonical(t) for t in BUILD_TOOLS}]
139+
140+
total_runtime = sum(sz for _, sz in runtime_dists)
141+
print(f"Runtime tree: {total_runtime/BYTES_IN_MB:.2f} MB" + (" (includes build tools)" if include_tools else ""))
142+
143+
if show_breakdown:
144+
print("\nBreakdown (descending):")
145+
for name, sz in runtime_dists:
146+
print(f" {name:<25} {sz/BYTES_IN_MB:7.2f} MB")
147+
148+
149+
if __name__ == "__main__":
150+
p = argparse.ArgumentParser(description="Measure on-disk size of a Python package built from source.")
151+
p.add_argument("source", help="Path, git/https URL, or anything pip understands.")
152+
p.add_argument("--no-deps", action="store_true", help="Skip installing dependencies inside the tmp venv.")
153+
p.add_argument("--breakdown", "-b", action="store_true", help="Show per-package size contribution (runtime only).")
154+
p.add_argument("--include-tools", action="store_true", help="Include build tools (pip/setuptools/wheel) in totals and table.")
155+
args = p.parse_args()
156+
main(args.source, include_deps=not args.no_deps, show_breakdown=args.breakdown, include_tools=args.include_tools)

examples/mcp_timeout_bug_demo.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
examples/mcp_timeout_bug_demo.py (cleaned)
44
55
Minimal demo that verifies the MCP timeout / retry behaviour
6-
without the heavyweight tracing that was needed during the bug hunt.
6+
without the heavy-weight tracing that was needed during the bug hunt.
77
88
Run it with
99
1010
$ python examples/mcp_timeout_bug_demo.py
1111
1212
You should see each step finishing in ~timeout seconds rather than the full
13-
20second hang simulated by the mock transport.
13+
20-second hang simulated by the mock transport.
1414
"""
1515

1616
from __future__ import annotations
@@ -23,7 +23,7 @@
2323
from typing import Dict, List, Any
2424

2525
# ---------------------------------------------------------------------------
26-
# Local imports add project root so `python examples/...` works everywhere.
26+
# Local imports - add project root so `python examples/...` works everywhere.
2727
# ---------------------------------------------------------------------------
2828
PROJECT_ROOT = Path(__file__).resolve().parents[1]
2929
sys.path.insert(0, str(PROJECT_ROOT))
@@ -36,12 +36,12 @@
3636
# ---------------------------------------------------------------------------
3737

3838
class _MockTransport:
39-
"""Transport that *never* responds within the callersupplied timeout."""
39+
"""Transport that *never* responds within the caller-supplied timeout."""
4040

41-
async def initialize(self) -> bool: # noqa: D401 simple bool
41+
async def initialize(self) -> bool: # noqa: D401 - simple bool
4242
return True
4343

44-
async def close(self) -> None: # noqa: D401 just a noop
44+
async def close(self) -> None: # noqa: D401 - just a noop
4545
return None
4646

4747
async def send_ping(self) -> bool: # keep StreamManager happy
@@ -71,7 +71,7 @@ async def _patched_stream_manager():
7171

7272
from chuk_tool_processor.mcp.stream_manager import StreamManager
7373

74-
async def _factory( # type: ignore[override] signature comes from classmethod
74+
async def _factory( # type: ignore[override] - signature comes from classmethod
7575
cls, servers, server_names=None
7676
):
7777
mgr = StreamManager()
@@ -115,7 +115,7 @@ async def _run_demo() -> None:
115115
# -------------------------------------------------------------------
116116
# 1️⃣ Processor.process (XML) with explicit timeout
117117
# -------------------------------------------------------------------
118-
print("\n1️⃣ processor.process() expect ~3s timeout")
118+
print("\n1️⃣ processor.process() - expect ~3s timeout")
119119
start = time.perf_counter()
120120
result = await processor.process(
121121
'<tool name="mcp.hanging_tool" args="{\"message\": \"hello\"}"/>',
@@ -127,7 +127,7 @@ async def _run_demo() -> None:
127127
# -------------------------------------------------------------------
128128
# 2️⃣ Processor.execute (ToolCall) with explicit timeout
129129
# -------------------------------------------------------------------
130-
print("\n2️⃣ processor.execute() expect ~1s timeout")
130+
print("\n2️⃣ processor.execute() - expect ~1s timeout")
131131
tc = ToolCall(tool="hanging_tool", namespace="mcp", arguments={})
132132
start = time.perf_counter()
133133
result = await processor.execute([tc], timeout=1.0)
@@ -137,7 +137,7 @@ async def _run_demo() -> None:
137137
# -------------------------------------------------------------------
138138
# 3️⃣ StreamManager.call_tool with timeout parameter
139139
# -------------------------------------------------------------------
140-
print("\n3️⃣ stream_manager.call_tool() expect ~2s timeout")
140+
print("\n3️⃣ stream_manager.call_tool() - expect ~2s timeout")
141141
start = time.perf_counter()
142142
sm_result = await stream_manager.call_tool(
143143
"hanging_tool", {"message": "hi"}, timeout=2.0
@@ -153,7 +153,7 @@ async def _run_demo() -> None:
153153
# ---------------------------------------------------------------------------
154154

155155
def _report(err: str | None, elapsed: float, *, expect: float) -> None:
156-
"""Prettyprint outcome and highlight if expectation wasn’t met."""
156+
"""Pretty-print outcome and highlight if expectation wasn’t met."""
157157

158158
status = "✅ OK" if err and abs(elapsed - expect) < 0.5 else "⚠️ ISSUE"
159159
print(f" · elapsed {_pretty(elapsed)}{status}; error=\"{err}\"")

src/chuk_tool_processor/execution/wrappers/retry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
Async-native retry wrapper for tool execution.
44
5-
Adds exponentialback-off retry logic and *deadline-aware* timeout handling so a
5+
Adds exponential-back-off retry logic and *deadline-aware* timeout handling so a
66
`timeout=` passed by callers is treated as the **total wall-clock budget** for
77
all attempts of a single tool call.
88
"""

tests/plugins/parsers/test_openai_tool.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# tests/tool_processor/plugins/parsers/test_openai_tool.py
22
"""OpenAI *tool_calls* parser plugin.
33
4-
Maps ChatCompletions native tool calls back into *ToolCall* objects using
4+
Maps Chat-Completions native tool calls back into *ToolCall* objects using
55
``registry.tool_export.tool_by_openai_name``. The import is done lazily
6-
inside ``try_parse`` to avoid circularimport issues revealed by tests.
6+
inside ``try_parse`` to avoid circular-import issues revealed by tests.
77
"""
88
from __future__ import annotations
99

@@ -18,7 +18,7 @@
1818

1919

2020
class OpenAIToolPlugin(ParserPlugin):
21-
"""Convert ChatCompletions *tool_calls* to internal *ToolCall*s."""
21+
"""Convert Chat-Completions *tool_calls* to internal *ToolCall*s."""
2222

2323
# ------------------------------------------------------------------
2424
def try_parse(self, raw: str | Any) -> List[ToolCall]: # type: ignore[override]

0 commit comments

Comments
 (0)