Skip to content

Commit fef26da

Browse files
committed
Add mypyc build script for compiling modules
- Add build_mypyc.py with --clean and --bench flags - Compile 11 modules: scalars, lexer, parser, predicates, coerce_input_value, value_from_ast, ast_from_value, type_from_ast, collect_fields, values - Add type annotation fix in coerce_input_value.py for mypyc - Document excluded modules and their limitations - Parser benchmark: ~30% faster (676μs vs 888μs median)
1 parent 850b30b commit fef26da

3 files changed

Lines changed: 1276 additions & 1 deletion

File tree

build_mypyc.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python
2+
"""Build script for mypyc compilation of graphql modules.
3+
4+
Usage:
5+
python build_mypyc.py # Build all configured modules
6+
python build_mypyc.py --clean # Remove compiled extensions
7+
python build_mypyc.py --bench # Run benchmark comparison
8+
9+
Benchmarks show ~18x speedup for recursive functions and ~16x for loops.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
import os
16+
import shutil
17+
import sys
18+
from pathlib import Path
19+
20+
# Modules to compile with mypyc.
21+
# Excluded due to mypyc limitations:
22+
# definition.py - conditional class definitions
23+
# execute.py - complex async triggers code-gen bugs
24+
# visitor.py - designed for subclassing
25+
# block_string.py - duck typing with lazy strings
26+
# type_comparators.py - type issues (list vs tuple)
27+
MYPYC_MODULES = [
28+
"graphql_mypyc/_sentinel.py",
29+
"graphql/type/scalars.py",
30+
"graphql/language/lexer.py",
31+
"graphql/language/parser.py",
32+
"graphql/language/predicates.py",
33+
"graphql/utilities/coerce_input_value.py",
34+
"graphql/utilities/value_from_ast.py",
35+
"graphql/utilities/ast_from_value.py",
36+
"graphql/utilities/type_from_ast.py",
37+
"graphql/execution/collect_fields.py",
38+
"graphql/execution/values.py",
39+
]
40+
41+
42+
def clean() -> None:
43+
"""Remove all compiled .so files from the source tree."""
44+
src = Path("src")
45+
if not src.exists():
46+
src = Path(".")
47+
48+
count = 0
49+
for so_file in src.rglob("*.so"):
50+
print(f"Removing {so_file}")
51+
so_file.unlink()
52+
count += 1
53+
54+
# Also clean build artifacts
55+
for dirname in ["build", ".mypy_cache"]:
56+
dirpath = Path(dirname)
57+
if dirpath.exists():
58+
print(f"Removing {dirpath}/")
59+
shutil.rmtree(dirpath)
60+
61+
print(f"Cleaned {count} .so files")
62+
63+
64+
def build() -> int:
65+
"""Compile modules with mypyc."""
66+
if not MYPYC_MODULES:
67+
print("No modules configured for compilation")
68+
return 0
69+
70+
# Change to src directory for proper module resolution
71+
original_dir = os.getcwd()
72+
src_dir = Path("src")
73+
if src_dir.exists():
74+
os.chdir(src_dir)
75+
76+
try:
77+
# Verify all modules exist
78+
for module in MYPYC_MODULES:
79+
if not Path(module).exists():
80+
print(f"Error: {module} not found")
81+
return 1
82+
83+
print(f"Compiling {len(MYPYC_MODULES)} modules with mypyc...")
84+
print("Modules:", MYPYC_MODULES)
85+
86+
try:
87+
from mypyc.build import mypycify
88+
except ImportError:
89+
print("Error: mypyc not installed. Install with: pip install mypy")
90+
return 1
91+
92+
# Use mypycify to get extension modules
93+
try:
94+
ext_modules = mypycify(
95+
MYPYC_MODULES,
96+
opt_level="3",
97+
debug_level="0",
98+
)
99+
except Exception as e:
100+
print(f"mypycify failed: {e}")
101+
return 1
102+
103+
if not ext_modules:
104+
print("No extension modules generated")
105+
return 1
106+
107+
# Build the extensions in-place
108+
from setuptools import Distribution
109+
from setuptools.command.build_ext import build_ext
110+
111+
dist = Distribution({"ext_modules": ext_modules})
112+
cmd = build_ext(dist)
113+
cmd.inplace = True
114+
cmd.ensure_finalized()
115+
116+
try:
117+
cmd.run()
118+
except Exception as e:
119+
print(f"Build failed: {e}")
120+
return 1
121+
122+
print("\nBuild successful!")
123+
# Show what was built
124+
for module in MYPYC_MODULES:
125+
path = Path(module)
126+
for so_file in path.parent.glob(f"{path.stem}*.so"):
127+
print(f" Built: {so_file}")
128+
129+
return 0
130+
131+
finally:
132+
os.chdir(original_dir)
133+
134+
135+
def benchmark() -> int:
136+
"""Run benchmark comparing interpreted vs compiled."""
137+
import time
138+
import importlib.util
139+
140+
print("=== Compiled (graphql_mypyc._sentinel) ===")
141+
142+
from graphql_mypyc import _sentinel as compiled
143+
144+
print(f"Module: {compiled.__file__}")
145+
print(f"is_compiled: {compiled.is_compiled()}")
146+
147+
# Test fibonacci
148+
n = 30
149+
start = time.perf_counter()
150+
result = compiled.fibonacci(n)
151+
elapsed_compiled_fib = time.perf_counter() - start
152+
print(f"fibonacci({n}) = {result} in {elapsed_compiled_fib:.4f}s")
153+
154+
# Test sum_squares
155+
n = 1_000_000
156+
start = time.perf_counter()
157+
result = compiled.sum_squares(n)
158+
elapsed_compiled_sq = time.perf_counter() - start
159+
print(f"sum_squares({n}) = {result} in {elapsed_compiled_sq:.6f}s")
160+
161+
print()
162+
print("=== Interpreted (graphql._sentinel) ===")
163+
164+
# Import the interpreted version directly by path
165+
spec = importlib.util.spec_from_file_location(
166+
"_sentinel_interp", "src/graphql/_sentinel.py"
167+
)
168+
interp = importlib.util.module_from_spec(spec)
169+
spec.loader.exec_module(interp) # type: ignore[union-attr]
170+
print(f"Module: {interp.__file__}")
171+
print(f"is_compiled: {interp.is_compiled()}")
172+
173+
# Test fibonacci
174+
n = 30
175+
start = time.perf_counter()
176+
result = interp.fibonacci(n)
177+
elapsed_interp_fib = time.perf_counter() - start
178+
print(f"fibonacci({n}) = {result} in {elapsed_interp_fib:.4f}s")
179+
180+
# Test sum_squares
181+
n = 1_000_000
182+
start = time.perf_counter()
183+
result = interp.sum_squares(n)
184+
elapsed_interp_sq = time.perf_counter() - start
185+
print(f"sum_squares({n}) = {result} in {elapsed_interp_sq:.6f}s")
186+
187+
print()
188+
print("=== Speedup ===")
189+
print(f"fibonacci: {elapsed_interp_fib/elapsed_compiled_fib:.1f}x faster")
190+
print(f"sum_squares: {elapsed_interp_sq/elapsed_compiled_sq:.1f}x faster")
191+
192+
return 0
193+
194+
195+
def main() -> int:
196+
"""CLI entry point."""
197+
parser = argparse.ArgumentParser(description="Build graphql modules with mypyc")
198+
parser.add_argument(
199+
"--clean", action="store_true", help="Remove compiled extensions"
200+
)
201+
parser.add_argument(
202+
"--bench", action="store_true", help="Run benchmark comparison"
203+
)
204+
args = parser.parse_args()
205+
206+
if args.clean:
207+
clean()
208+
return 0
209+
210+
if args.bench:
211+
return benchmark()
212+
213+
return build()
214+
215+
216+
if __name__ == "__main__":
217+
sys.exit(main())

0 commit comments

Comments
 (0)