From 82ce66a951937bb528e4677b40345b9a18880a95 Mon Sep 17 00:00:00 2001 From: Perry Zhu Date: Tue, 19 May 2026 18:02:31 -0700 Subject: [PATCH 1/2] fix: system resource detection on windows #59 --- CHANGELOG.md | 3 ++ src/clawbench/runner/batch.py | 1 + src/clawbench/tui.py | 55 +++++++++++++++++++++++++++++++---- tests/test_tui_helpers.py | 42 ++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798bbd7f..f2c1bef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Changed - More data are stored in the `run-meta.json` for better post-hoc analysis and reproducibility, including the hash of the configs, runtime info, and flags used. +### Fixed +- Fixed several compatibility issues on Windows platforms. + ## [0.3.2] - 2026-05-15 ### Added - Added the logic to remove the `.log` files from the generated `data/` directory to remove noise. diff --git a/src/clawbench/runner/batch.py b/src/clawbench/runner/batch.py index fb27718c..ef2d5a48 100644 --- a/src/clawbench/runner/batch.py +++ b/src/clawbench/runner/batch.py @@ -630,6 +630,7 @@ async def async_main(args: argparse.Namespace) -> int: def on_signal() -> None: nonlocal sigint_count + assert shutdown_event is not None sigint_count += 1 shutdown_event.set() diff --git a/src/clawbench/tui.py b/src/clawbench/tui.py index 9444f40b..7b95257c 100644 --- a/src/clawbench/tui.py +++ b/src/clawbench/tui.py @@ -384,13 +384,55 @@ def load_cases(cases_dir_name: str = "test-cases") -> list[str]: # --------------------------------------------------------------------------- -def _recommend_concurrent() -> int: - cpus = multiprocessing.cpu_count() +def _windows_physical_memory_gb() -> float | None: + try: + import ctypes + except ImportError: + return None + + windll = getattr(ctypes, "windll", None) + if windll is None: + return None + + class MEMORYSTATUSEX(ctypes.Structure): + _fields_ = [ + ("dwLength", ctypes.c_ulong), + ("dwMemoryLoad", ctypes.c_ulong), + ("ullTotalPhys", ctypes.c_ulonglong), + ("ullAvailPhys", ctypes.c_ulonglong), + ("ullTotalPageFile", ctypes.c_ulonglong), + ("ullAvailPageFile", ctypes.c_ulonglong), + ("ullTotalVirtual", ctypes.c_ulonglong), + ("ullAvailVirtual", ctypes.c_ulonglong), + ("ullAvailExtendedVirtual", ctypes.c_ulonglong), + ] + + status = MEMORYSTATUSEX() + status.dwLength = ctypes.sizeof(status) + try: + ok = windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)) + except (AttributeError, OSError): + return None + if not ok: + return None + return status.ullTotalPhys / (1024**3) + + +def _physical_memory_gb() -> float: + if platform.system() == "Windows": + mem_gb = _windows_physical_memory_gb() + if mem_gb is not None: + return mem_gb try: mem_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") - mem_gb = mem_bytes / (1024**3) - except (ValueError, OSError): - mem_gb = 8 + return mem_bytes / (1024**3) + except (AttributeError, ValueError, OSError): + return 8 + + +def _recommend_concurrent() -> int: + cpus = multiprocessing.cpu_count() + mem_gb = _physical_memory_gb() by_cpu = cpus // 2 by_ram = int(mem_gb // 2) recommended = max(1, min(by_cpu, by_ram)) @@ -1270,7 +1312,7 @@ def _run_streamed(cmd: list[str], *, status_msg: str) -> int: return rc -def _fix_engine(engine: str, status: str, detail: str) -> bool: +def _fix_engine(engine: str | None, status: str, detail: str) -> bool: """Show an actionable panel for the engine problem and offer a fix. Returns True if the engine is now usable, False otherwise. Safe to @@ -1530,6 +1572,7 @@ def main() -> None: console.print() console.print("[bold]Welcome to ClawBench.[/]") theme = _pick_theme() + assert theme is not None STYLE = _make_style(theme) # Apple HIG: Indigo for headers, Blue for inline accents if theme == "light": diff --git a/tests/test_tui_helpers.py b/tests/test_tui_helpers.py index 96f7588e..47b5dfb5 100644 --- a/tests/test_tui_helpers.py +++ b/tests/test_tui_helpers.py @@ -4,7 +4,9 @@ import subprocess import sys +import types from pathlib import Path +from typing import Any import pytest @@ -242,6 +244,46 @@ def test_tui_recommend_concurrent_returns_positive_value( assert tui._recommend_concurrent() >= 1 +def test_tui_recommend_concurrent_uses_windows_memory( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(tui.platform, "system", lambda: "Windows") + monkeypatch.setattr(tui.multiprocessing, "cpu_count", lambda: 16) + monkeypatch.setattr(tui, "_windows_physical_memory_gb", lambda: 4) + + assert tui._recommend_concurrent() == 2 + + +def test_tui_windows_physical_memory_uses_global_memory_status( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class Kernel32: + def GlobalMemoryStatusEx(self, status: Any) -> int: + status.ullTotalPhys = 12 * 1024**3 + return 1 + + fake_ctypes: Any = types.ModuleType("ctypes") + fake_ctypes.Structure = object + fake_ctypes.c_ulong = object() + fake_ctypes.c_ulonglong = object() + fake_ctypes.sizeof = lambda _status: 64 + fake_ctypes.byref = lambda status: status + fake_ctypes.windll = types.SimpleNamespace(kernel32=Kernel32()) + monkeypatch.setitem(sys.modules, "ctypes", fake_ctypes) + + assert tui._windows_physical_memory_gb() == 12 + + +def test_tui_physical_memory_falls_back_when_unavailable( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(tui.platform, "system", lambda: "Windows") + monkeypatch.setattr(tui, "_windows_physical_memory_gb", lambda: None) + monkeypatch.delattr(tui.os, "sysconf", raising=False) + + assert tui._physical_memory_gb() == 8 + + def test_tui_main_single_run_flow_builds_runner_command( monkeypatch: pytest.MonkeyPatch, ) -> None: From 30e4b2e33a26ff32e42b13a42f87bb0dda3b5182 Mon Sep 17 00:00:00 2001 From: Perry Zhu Date: Tue, 19 May 2026 18:05:48 -0700 Subject: [PATCH 2/2] ci: pyright check --- .github/workflows/static-check.yml | 3 +++ .pre-commit-config.yaml | 7 +++++++ pyproject.toml | 9 +++++++++ uv.lock | 15 +++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/.github/workflows/static-check.yml b/.github/workflows/static-check.yml index 1eca7571..d0e3b31d 100644 --- a/.github/workflows/static-check.yml +++ b/.github/workflows/static-check.yml @@ -46,6 +46,9 @@ jobs: - name: Ruff format check run: uv run --frozen ruff format --check . + - name: Pyright check + run: uv run --frozen pyright src/clawbench tests + - name: Build package env: UV_FROZEN: "true" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f92d6ff..520d332f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,3 +5,10 @@ repos: - id: ruff-check - id: ruff-format args: [--check] + - repo: local + hooks: + - id: pyright + name: pyright + entry: uv run --frozen pyright src/clawbench tests + language: system + pass_filenames: false diff --git a/pyproject.toml b/pyproject.toml index dde46430..499326c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,10 +62,19 @@ exclude = [ [tool.ruff] target-version = "py311" +[tool.pyright] +include = ["src/clawbench", "tests"] +exclude = [ + ".venv", + "src/clawbench/runtime/extension-server", + "src/clawbench/runtime/harnesses", +] + [dependency-groups] dev = [ "jsonschema>=4.26.0", "pre-commit>=4.6.0", + "pyright>=1.1.407", "pytest>=9.0.3", "ruff>=0.15.12", ] diff --git a/uv.lock b/uv.lock index 4829c56f..8a2b0ffc 100644 --- a/uv.lock +++ b/uv.lock @@ -72,6 +72,7 @@ dependencies = [ dev = [ { name = "jsonschema" }, { name = "pre-commit" }, + { name = "pyright" }, { name = "pytest" }, { name = "ruff" }, ] @@ -89,6 +90,7 @@ requires-dist = [ dev = [ { name = "jsonschema", specifier = ">=4.26.0" }, { name = "pre-commit", specifier = ">=4.6.0" }, + { name = "pyright", specifier = ">=1.1.407" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "ruff", specifier = ">=0.15.12" }, ] @@ -537,6 +539,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyright" +version = "1.1.409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, +] + [[package]] name = "pytest" version = "9.0.3"