Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 64 additions & 57 deletions benchmarks/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@

import argparse
import asyncio
import sys
import statistics
import sys
from pathlib import Path

# Add project root to path for pyisolate imports
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))

from benchmark_harness import BenchmarkHarness
from pyisolate import ProxiedSingleton, ExtensionBase, ExtensionConfig, local_execution
from benchmark_harness import BenchmarkHarness # noqa: E402

from pyisolate import ExtensionBase, ExtensionConfig, ProxiedSingleton # noqa: E402

try:
import torch
TORCH_AVAILABLE = True
except ImportError:
from importlib.util import find_spec

TORCH_AVAILABLE = find_spec("torch") is not None
except ImportError: # pragma: no cover
TORCH_AVAILABLE = False

try:
from tabulate import tabulate

TABULATE_AVAILABLE = True
except ImportError:
TABULATE_AVAILABLE = False
Expand All @@ -36,8 +39,10 @@
# Host-side Classes
# =============================================================================


class DatabaseSingleton(ProxiedSingleton):
"""Simple dictionary-based singleton for testing state."""

def __init__(self):
self._db = {}

Expand All @@ -52,11 +57,12 @@ class BenchmarkExtensionWrapper(ExtensionBase):
"""
Host-side wrapper that proxies calls to the isolated extension.
"""

async def on_module_loaded(self, module):
"""Called when the isolated module is loaded."""
if not getattr(module, "benchmark_entrypoint", None):
raise RuntimeError(f"Module {module.__name__} missing 'benchmark_entrypoint'")

# Instantiate the child-side extension object
self.extension = module.benchmark_entrypoint()
await self.extension.initialize()
Expand Down Expand Up @@ -89,7 +95,7 @@ async def get_value(self, key): pass

class BenchmarkExtension:
"""Child-side extension implementation."""

async def initialize(self):
pass

Expand Down Expand Up @@ -179,30 +185,29 @@ def __init__(self, mean, stdev, min_time, max_time):

class SimpleRunner:
"""Minimal runner to replace TestRPCBenchmarks.runner."""

def __init__(self, warmup_runs=5, benchmark_runs=1000):
self.warmup_runs = warmup_runs
self.benchmark_runs = benchmark_runs

async def run_benchmark(self, name, func):
import time

times = []

# Warmup
for _ in range(self.warmup_runs):
await func()

# Benchmark
for _ in range(self.benchmark_runs):
start = time.perf_counter()
await func()
end = time.perf_counter()
times.append(end - start)

return BenchmarkResult(
statistics.mean(times),
statistics.stdev(times) if len(times) > 1 else 0,
min(times),
max(times)
statistics.mean(times), statistics.stdev(times) if len(times) > 1 else 0, min(times), max(times)
)


Expand All @@ -211,13 +216,13 @@ async def run_benchmarks(
):
print("PyIsolate RPC Benchmark Suite (Refactored for 1.0)")
print("=" * 60)

harness = BenchmarkHarness()
await harness.setup_test_environment("benchmark")

runner = SimpleRunner(
warmup_runs=2 if quick else 5,
benchmark_runs=100 if quick else 1000
warmup_runs=2 if quick else 5,
benchmark_runs=100 if quick else 1000,
)

try:
Expand All @@ -230,7 +235,7 @@ async def run_benchmarks(
"benchmark_ext",
dependencies=["numpy>=1.26.0", "torch>=2.0.0"] if torch_available else ["numpy>=1.26.0"],
share_torch=False,
extension_code=BENCHMARK_EXTENSION_CODE
extension_code=BENCHMARK_EXTENSION_CODE,
)
extensions_config.append({"name": "benchmark_ext", "share": False})

Expand All @@ -239,67 +244,70 @@ async def run_benchmarks(
"benchmark_ext_shared",
dependencies=["numpy>=1.26.0", "torch>=2.0.0"],
share_torch=True,
extension_code=BENCHMARK_EXTENSION_CODE
extension_code=BENCHMARK_EXTENSION_CODE,
)
extensions_config.append({"name": "benchmark_ext_shared", "share": True})

# Load Extensions using Manager
manager = harness.get_manager(BenchmarkExtensionWrapper)

ext_standard = None
ext_shared = None

for cfg in extensions_config:
name = cfg["name"]
share_torch = cfg["share"]
print(f"Loading extension {name} (share_torch={share_torch})...")

# Reconstruct minimal deps for config (manager uses this for venv check/install)
deps = ["numpy>=1.26.0"]
if torch_available: deps.append("torch>=2.0.0")

if torch_available:
deps.append("torch>=2.0.0")

config = ExtensionConfig(
name=name,
module_path=str(harness.test_root / "extensions" / name),
isolated=True,
dependencies=deps,
apis=[DatabaseSingleton], # Host must allow the singleton
share_torch=share_torch
apis=[DatabaseSingleton], # Host must allow the singleton
share_torch=share_torch,
)

ext = manager.load_extension(config)
if name == "benchmark_ext":
ext_standard = ext
else:
ext_shared = ext

print("Extensions loaded.\n")

# Define Test Data
test_data = [
("small_int", 42),
("small_string", "hello world"),
]

runner_results = {}

# --- Run Benchmarks ---
# Note: In a full implementation, we'd replicate the comprehensive test suite.
# Here we verify core functionality by running the 'do_stuff' generic method.
# This confirms RPC, Serialization, and Process Isolation are working.

target_extensions = []
if ext_standard: target_extensions.append(("Standard", ext_standard))
if ext_shared: target_extensions.append(("Shared", ext_shared))

for name, ext in target_extensions:
print(f"--- Benchmarking {name} Mode ---")
if ext_standard:
target_extensions.append(("Standard", ext_standard))
if ext_shared:
target_extensions.append(("Shared", ext_shared))

for mode_name, ext in target_extensions:
print(f"--- Benchmarking {mode_name} Mode ---")
for data_name, data_val in test_data:
bench_name = f"{name}_{data_name}"
async def func():
return await ext.do_stuff(data_val)
bench_name = f"{mode_name}_{data_name}"

async def func(bound_ext=ext, bound_value=data_val):
return await bound_ext.do_stuff(bound_value)

print(f"Running {bench_name}...")
try:
res = await runner.run_benchmark(bench_name, func)
Expand All @@ -311,12 +319,12 @@ async def func():
print("\n" + "=" * 60)
print("RESULTS")
print("=" * 60)

headers = ["Test", "Mean (ms)", "Std Dev (ms)"]
table_data = []
for name, res in runner_results.items():
table_data.append([name, f"{res.mean*1000:.3f}", f"{res.stdev*1000:.3f}"])
table_data.append([name, f"{res.mean * 1000:.3f}", f"{res.stdev * 1000:.3f}"])

if TABULATE_AVAILABLE:
print(tabulate(table_data, headers=headers))
else:
Expand All @@ -325,27 +333,26 @@ async def func():

finally:
await harness.cleanup()

return 0


def main():
def main() -> int:
parser = argparse.ArgumentParser(description="PyIsolate 1.0 Benchmark")
parser.add_argument("--quick", action="store_true")
parser.add_argument("--no-torch", action="store_true")
parser.add_argument("--no-gpu", action="store_true")
parser.add_argument("--torch-mode", default="both")

args = parser.parse_args()

try:
import numpy
import psutil
except ImportError:

if find_spec("numpy") is None or find_spec("psutil") is None:
print("Please install dependencies: pip install numpy psutil tabulate")
return 1

asyncio.run(run_benchmarks(args.quick, args.no_torch, args.no_gpu, args.torch_mode))
return 0
Comment on lines +349 to +354
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error message lists optional dependency; run_benchmarks return value is ignored.

Two minor issues:

  1. Line 350 mentions tabulate in the required dependencies message, but tabulate is optional (handled gracefully via TABULATE_AVAILABLE). Consider updating the message to only list truly required dependencies.

  2. Line 353 calls run_benchmarks() which returns 0, but this return value is ignored. Line 354 unconditionally returns 0. If run_benchmarks is intended to signal success/failure via its return value, propagate it.

Proposed fix
     if find_spec("numpy") is None or find_spec("psutil") is None:
-        print("Please install dependencies: pip install numpy psutil tabulate")
+        print("Please install dependencies: pip install numpy psutil")
+        print("Optional: pip install tabulate (for formatted output)")
         return 1

-    asyncio.run(run_benchmarks(args.quick, args.no_torch, args.no_gpu, args.torch_mode))
-    return 0
+    return asyncio.run(run_benchmarks(args.quick, args.no_torch, args.no_gpu, args.torch_mode))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if find_spec("numpy") is None or find_spec("psutil") is None:
print("Please install dependencies: pip install numpy psutil tabulate")
return 1
asyncio.run(run_benchmarks(args.quick, args.no_torch, args.no_gpu, args.torch_mode))
return 0
if find_spec("numpy") is None or find_spec("psutil") is None:
print("Please install dependencies: pip install numpy psutil")
print("Optional: pip install tabulate (for formatted output)")
return 1
return asyncio.run(run_benchmarks(args.quick, args.no_torch, args.no_gpu, args.torch_mode))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/benchmark.py` around lines 349 - 354, Update the dependency check
message to only list truly required packages (remove "tabulate" from the printed
string) and propagate the exit code returned by run_benchmarks: call result =
asyncio.run(run_benchmarks(args.quick, args.no_torch, args.no_gpu,
args.torch_mode)) and return that result instead of unconditionally returning 0.
Locate the check using find_spec and the run_benchmarks invocation to make these
changes; ensure run_benchmarks indeed returns an int and keep TABULATE_AVAILABLE
handling unchanged.



if __name__ == "__main__":
main()
raise SystemExit(main())
Loading
Loading