diff --git a/_c131_cpu.py b/_c131_cpu.py deleted file mode 100644 index 4b2d79b..0000000 --- a/_c131_cpu.py +++ /dev/null @@ -1,94 +0,0 @@ -"""CPU-time attribution for C131 (wall clock on this box is too noisy). - -process_time() counts CPU across every thread of the process, so scheduler -jitter and the thread-pool handoff latency drop out and what is left is the -actual work each _async shape performs. -""" -from __future__ import annotations - -import asyncio -import os -import statistics -import time - -os.environ.setdefault("AUTOCAD_MCP_BACKEND", "ezdxf") - -import config # noqa: E402 -from backends.ezdxf_backend import EzdxfBackend # noqa: E402 - -CALLS = 4000 -REPS = 9 -SHIPPED = EzdxfBackend._async - - -def noop(): - return None - - -async def task_no_waitfor(self, func, *a, **k): - timeout = config.settings.ezdxf_call_timeout - loop = asyncio.get_running_loop() - deadline = loop.time() + timeout - _ = max(deadline - loop.time(), 0.0) - lock = self._lock - await lock.acquire() - call = asyncio.ensure_future(asyncio.to_thread(func, *a, **k)) - try: - result = await call - except BaseException: - lock.release() - raise - lock.release() - return result - - -async def naive_waitfor(self, func, *a, **k): - timeout = config.settings.ezdxf_call_timeout - async with self._lock: - return await asyncio.wait_for(asyncio.to_thread(func, *a, **k), timeout) - - -VARIANTS = [ - ("A shipped timeout path (2x wait_for + ensure_future)", SHIPPED, 120.0), - ("B shipped no-timeout path (lock + to_thread) ", SHIPPED, 0.0), - ("C timeout path, both wait_for removed ", task_no_waitfor, 120.0), - ("D naive single wait_for on the coroutine ", naive_waitfor, 120.0), -] - - -async def run(impl, timeout, backend): - EzdxfBackend._async = impl - config.settings.ezdxf_call_timeout = timeout - try: - started = time.process_time() - for _ in range(CALLS): - await backend._async(noop) - return (time.process_time() - started) * 1e6 / CALLS - finally: - EzdxfBackend._async = SHIPPED - - -async def main(): - backend = EzdxfBackend() - await backend.connect() - await run(SHIPPED, 120.0, backend) - samples = {label: [] for label, _, _ in VARIANTS} - for _ in range(REPS): - for label, impl, timeout in VARIANTS: - samples[label].append(await run(impl, timeout, backend)) - med = {} - for label, _, _ in VARIANTS: - vals = sorted(samples[label]) - med[label] = statistics.median(vals) - print(f"{label} {med[label]:7.1f} us cpu/call min {vals[0]:.0f} max {vals[-1]:.0f}") - a, b, c, d = (med[label] for label, _, _ in VARIANTS) - print() - print(f"timeout path overhead A-B : {a - b:6.1f} us cpu/call") - print(f"survives removing wait_for C-B : {c - b:6.1f} us cpu/call " - f"({(c - b) / (a - b) * 100:.0f}% of A-B)") - print(f"attributable to wait_for A-C : {a - c:6.1f} us cpu/call " - f"({(a - c) / (a - b) * 100:.0f}% of A-B)") - print(f"naive single wait_for D-B : {d - b:6.1f} us cpu/call") - - -asyncio.run(main()) diff --git a/_c131_micro.py b/_c131_micro.py deleted file mode 100644 index 012e12e..0000000 --- a/_c131_micro.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Micro-attribution for README claim C131: what does the timeout path cost? - -Same four `_async` shapes, but the payload is a no-op so the delta is pure -async machinery. Interleaved, median of many reps. -""" -from __future__ import annotations - -import asyncio -import os -import statistics -import time - -os.environ.setdefault("AUTOCAD_MCP_BACKEND", "ezdxf") - -import config # noqa: E402 -from backends.ezdxf_backend import EzdxfBackend # noqa: E402 - -CALLS = 3000 -REPS = 7 -SHIPPED = EzdxfBackend._async - - -def noop(): - return None - - -async def shipped_off(self, func, *a, **k): - async with self._lock: - return await asyncio.to_thread(func, *a, **k) - - -async def task_no_waitfor(self, func, *a, **k): - timeout = config.settings.ezdxf_call_timeout - loop = asyncio.get_running_loop() - deadline = loop.time() + timeout - _ = max(deadline - loop.time(), 0.0) - lock = self._lock - await lock.acquire() - call = asyncio.ensure_future(asyncio.to_thread(func, *a, **k)) - try: - result = await call - except BaseException: - lock.release() - raise - lock.release() - return result - - -async def naive_waitfor(self, func, *a, **k): - timeout = config.settings.ezdxf_call_timeout - async with self._lock: - return await asyncio.wait_for(asyncio.to_thread(func, *a, **k), timeout) - - -VARIANTS = [ - ("A shipped timeout path (2x wait_for + ensure_future)", SHIPPED, 120.0), - ("B shipped no-timeout path (lock + to_thread) ", SHIPPED, 0.0), - ("C timeout path with both wait_for removed ", task_no_waitfor, 120.0), - ("D naive single wait_for on the coroutine ", naive_waitfor, 120.0), -] - - -async def run(impl, timeout, backend): - EzdxfBackend._async = impl - config.settings.ezdxf_call_timeout = timeout - try: - started = time.perf_counter() - for _ in range(CALLS): - await backend._async(noop) - return (time.perf_counter() - started) * 1e6 / CALLS # us per call - finally: - EzdxfBackend._async = SHIPPED - - -async def main(): - backend = EzdxfBackend() - await backend.connect() - await run(SHIPPED, 120.0, backend) # warm-up - samples = {label: [] for label, _, _ in VARIANTS} - for _ in range(REPS): - for label, impl, timeout in VARIANTS: - samples[label].append(await run(impl, timeout, backend)) - med = {} - for label, _, _ in VARIANTS: - med[label] = statistics.median(samples[label]) - print(f"{label} {med[label]:7.1f} us/call runs " - + " ".join(f"{v:.0f}" for v in samples[label])) - a, b, c, d = (med[label] for label, _, _ in VARIANTS) - print() - print(f"timeout path total overhead A-B : {a - b:6.1f} us/call") - print(f"overhead with wait_for gone C-B : {c - b:6.1f} us/call " - f"({(c - b) / (a - b) * 100:.0f}% of it)") - print(f"cost of the wait_for wrappers A-C: {a - c:6.1f} us/call " - f"({(a - c) / (a - b) * 100:.0f}% of it)") - print(f"naive one-wait_for overhead D-B : {d - b:6.1f} us/call") - - -asyncio.run(main()) diff --git a/_c131_probe.py b/_c131_probe.py deleted file mode 100644 index 62ad5a5..0000000 --- a/_c131_probe.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Read-only probe for README claim C131 (asyncio.wait_for attribution). - -Interleaved A/B/C/D so warm-up and machine drift hit every variant equally. -""" -from __future__ import annotations - -import asyncio -import os -import statistics -import time - -os.environ.setdefault("AUTOCAD_MCP_BACKEND", "ezdxf") - -import config # noqa: E402 -from backends.ezdxf_backend import EzdxfBackend # noqa: E402 - -N = 2000 -REPS = 5 - -SHIPPED = EzdxfBackend._async - - -async def build(backend: EzdxfBackend) -> float: - await backend.drawing_new() - started = time.perf_counter() - for index in range(N): - y = float(index % 100) - await backend.entity_create_line(0.0, y, 100.0, y + 1.0) - return (time.perf_counter() - started) * 1000.0 - - -async def async_no_timeout(self, func, *args, **kwargs): - """The shipped timeout<=0 branch: plain lock + to_thread, no wait_for.""" - async with self._lock: - return await asyncio.to_thread(func, *args, **kwargs) - - -async def async_task_no_waitfor(self, func, *args, **kwargs): - """Shipped timeout branch MINUS both wait_for wrappers. - - Same lock acquire, same loop.time() bookkeeping, same ensure_future Task - around to_thread, same try/except/release -- only the two asyncio.wait_for - wrappers removed. - """ - timeout = config.settings.ezdxf_call_timeout - loop = asyncio.get_running_loop() - deadline = loop.time() + timeout - _ = max(deadline - loop.time(), 0.0) - lock = self._lock - await lock.acquire() - call = asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs)) - try: - result = await call - except BaseException: - lock.release() - raise - lock.release() - return result - - -async def async_waitfor_only(self, func, *args, **kwargs): - """Naive shape: plain `async with lock` + a single wait_for on the coroutine. - - This is what "wrap every call in asyncio.wait_for" would look like if you - wrote it the obvious way -- wait_for does its own ensure_future internally. - """ - timeout = config.settings.ezdxf_call_timeout - async with self._lock: - return await asyncio.wait_for(asyncio.to_thread(func, *args, **kwargs), timeout) - - -VARIANTS = [ - ("A shipped, EZDXF_CALL_TIMEOUT=120", SHIPPED, 120.0), - ("B shipped, EZDXF_CALL_TIMEOUT=0 ", SHIPPED, 0.0), - ("C timeout branch minus wait_for ", async_task_no_waitfor, 120.0), - ("D plain lock + to_thread ", async_no_timeout, 120.0), - ("E naive single wait_for ", async_waitfor_only, 120.0), -] - - -async def one(impl, timeout): - EzdxfBackend._async = impl - config.settings.ezdxf_call_timeout = timeout - backend = EzdxfBackend() - await backend.connect() - try: - return await build(backend) - finally: - EzdxfBackend._async = SHIPPED - - -async def main(): - print("python", os.sys.version.split()[0]) - print("default EZDXF_CALL_TIMEOUT =", config.settings.ezdxf_call_timeout) - await one(SHIPPED, 120.0) # warm-up, discarded - samples = {label: [] for label, _, _ in VARIANTS} - for _ in range(REPS): - for label, impl, timeout in VARIANTS: - samples[label].append(await one(impl, timeout)) - meds = {} - for label, _, _ in VARIANTS: - vals = samples[label] - meds[label] = statistics.median(vals) - print(f"{label} median {meds[label]:8.1f} ms runs " - + " ".join(f"{v:.0f}" for v in vals)) - a = meds["A shipped, EZDXF_CALL_TIMEOUT=120"] - b = meds["B shipped, EZDXF_CALL_TIMEOUT=0 "] - c = meds["C timeout branch minus wait_for "] - d = meds["D plain lock + to_thread "] - e = meds["E naive single wait_for "] - print() - print(f"naive wait_for over baseline (E-D) : {e - d:7.1f} ms ({(e/d-1)*100:5.1f}%)") - print(f"total timeout-branch overhead (A-B) : {a - b:7.1f} ms ({(a/b-1)*100:5.1f}%)") - print(f"overhead with wait_for removed (C-D): {c - d:7.1f} ms") - print(f"overhead attributable to wait_for : {a - c:7.1f} ms") - if a - b > 0: - print(f"wait_for share of the A-B delta : {(a - c)/(a - b)*100:5.0f}%") - - -asyncio.run(main())