Skip to content

Commit 55ed037

Browse files
authored
feat: embed Rust backend via maturin build system (#17)
* refactor: move gamecov-core Rust crate into monorepo * refactor: merge gamecov-core into single maturin-built package Replace hatchling with maturin as the build backend, embedding the Rust extension (gamecov-core) directly into the gamecov package. The compiled module is now installed as gamecov._gamecov_core. - Move Rust sources from gamecov-core/src/ to src/ - Add Cargo.toml at repo root with module-name config - Add type stub _gamecov_core.pyi (eliminates type: ignore) - Update CI to install Rust toolchain before build - Delete gamecov-core/ subdirectory Users can now install the full package with: pip install . * doc: rust code * feat: add pytest-benchmark suite for Python vs Rust monitor comparison * add python v.s. rust performance benchmark * ci: add Rust fmt, clippy, and test to CI and pre-commit hooks * doc: update for rust dev * doc: add Rust vs Python benchmark results to README * chore: bump version to 0.2.0
1 parent ea0d0d1 commit 55ed037

23 files changed

Lines changed: 1391 additions & 39 deletions

.github/workflows/pytest.yml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@ name: Unit Testing
33
on: [pull_request]
44

55
jobs:
6-
unittest:
7-
runs-on: ubuntu-latest
6+
unittest:
7+
runs-on: ubuntu-latest
88

9-
steps:
10-
- uses: actions/checkout@v2
11-
with:
12-
fetch-depth: 1
9+
steps:
10+
- uses: actions/checkout@v4
11+
with:
12+
fetch-depth: 1
1313

14-
- name: Install uv
15-
uses: astral-sh/setup-uv@v5
14+
- name: Install Rust
15+
uses: dtolnay/rust-toolchain@stable
1616

17-
- name: Set up Python
18-
run: uv sync
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
1919

20-
- name: Run Unit Tests
21-
run: uv run pytest -n auto
20+
- name: Set up Python
21+
run: uv sync
22+
23+
- name: Run Unit Tests
24+
run: uv run pytest -n auto

.github/workflows/rust.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Rust CI
2+
3+
on: [pull_request]
4+
5+
jobs:
6+
rust-ci:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
with:
12+
fetch-depth: 1
13+
14+
- name: Install Rust toolchain
15+
uses: dtolnay/rust-toolchain@stable
16+
with:
17+
components: rustfmt, clippy
18+
19+
- name: Rust cache
20+
uses: Swatinem/rust-cache@v2
21+
22+
- name: Check formatting
23+
run: cargo fmt --all -- --check
24+
25+
- name: Clippy
26+
run: cargo clippy --all-targets --all-features -- -D warnings
27+
28+
- name: Tests
29+
run: cargo test --all-features

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ assets/
1515
.vscode/
1616
.hypothesis/
1717
prof/
18-
.coverage
18+
.coverage
19+
20+
# Rust build artifacts
21+
target/
22+
Cargo.lock

.pre-commit-config.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,25 @@ repos:
1919
- id: ruff
2020
args: [--fix]
2121
- id: ruff-format
22+
23+
- repo: local
24+
hooks:
25+
- id: cargo-fmt
26+
name: cargo fmt
27+
entry: cargo fmt --all
28+
language: system
29+
types: [rust]
30+
pass_filenames: false
31+
- id: cargo-clippy
32+
name: cargo clippy
33+
entry: cargo clippy --all-targets --all-features -- -D warnings
34+
language: system
35+
types: [rust]
36+
pass_filenames: false
37+
- id: cargo-test
38+
name: cargo test
39+
entry: cargo test --all-features
40+
language: system
41+
types: [rust]
42+
pass_filenames: false
43+
stages: [pre-push]

AGENTS.md

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,29 @@ Future metrics (e.g., audio coverage, state-graph coverage) will follow the same
1313

1414
```
1515
.
16+
├── Cargo.toml # Rust manifest (maturin builds the extension)
17+
├── pyproject.toml # Project metadata, maturin build backend
1618
├── src/
1719
│ ├── main.py # CLI entry point (Typer)
20+
│ ├── lib.rs # PyO3 module entry point (Rust)
21+
│ ├── bktree.rs # BK-tree<u64> with POPCNT Hamming distance
22+
│ ├── unionfind.rs # Flat Vec-based union-find
23+
│ ├── monitor.rs # CoverageTracker (BK-tree + UnionFind combined)
1824
│ └── gamecov/
1925
│ ├── __init__.py # Public API re-exports
26+
│ ├── _gamecov_core.pyi # Type stub for Rust extension
2027
│ ├── cov_base.py # Abstract protocols: CoverageItem, Coverage, CoverageMonitor
2128
│ ├── frame.py # Frame dataclass (PIL Image wrapper with average-hash)
2229
│ ├── dedup.py # Deduplication algorithms (pHash, SSIM [deprecated])
23-
│ ├── frame_cov.py # FrameCoverage, FrameMonitor, BKFrameMonitor, BK-tree, UnionFind
30+
│ ├── frame_cov.py # FrameCoverage, FrameMonitor, BKFrameMonitor, RustBKFrameMonitor, BK-tree, UnionFind
2431
│ ├── loader.py # MP4 loading: bulk, lazy (generator), last-n
2532
│ ├── writer.py # MP4 writing: imageio and OpenCV backends
2633
│ ├── stitch.py # Panorama stitching of unique frames
2734
│ ├── generator.py # Hypothesis strategies for property-based testing
2835
│ ├── env.py # Runtime config (RADIUS env var)
2936
│ └── py.typed # PEP 561 marker
37+
├── rust-tests/
38+
│ └── prop_tests.rs # Rust proptest property-based tests
3039
├── tests/
3140
│ ├── test_generators.py # Frame/FrameList generation strategies
3241
│ ├── test_dedup.py # Dedup monotonicity properties
@@ -35,19 +44,34 @@ Future metrics (e.g., audio coverage, state-graph coverage) will follow the same
3544
│ ├── test_load_write_assets.py# Differential tests across loaders on real videos
3645
│ ├── test_monotone.py # Coverage monotonicity (FrameMonitor & BKFrameMonitor)
3746
│ ├── test_BK_frame_monitor.py # Differential: FrameMonitor vs BKFrameMonitor
47+
│ ├── test_rust_frame_monitor.py # Differential & monotonicity: BKFrameMonitor vs RustBKFrameMonitor
3848
│ └── test_monotone_smb.py # Real-world monotonicity on SMB dataset
49+
├── benchmarks/
50+
│ ├── conftest.py # Session-scoped fixtures (pre-generated FrameCoverage)
51+
│ └── test_bench_monitor.py # Python vs Rust monitor throughput benchmarks
3952
├── assets/
4053
│ ├── videos/ # Small sample MP4s for integration tests
4154
│ └── smb/ # Super Smash Bros recordings for stress tests
4255
├── docs/
4356
│ └── design.md # Architecture and design documentation
44-
├── pyproject.toml # Project metadata, dependencies, tool configs
45-
├── .pre-commit-config.yaml # Pre-commit hooks
46-
├── .github/workflows/ # CI: pytest, mypy, ruff, pylint
57+
├── rustfmt.toml # Rust formatting config
58+
├── .pre-commit-config.yaml # Pre-commit hooks (Python + Rust)
59+
├── .github/workflows/ # CI: pytest, mypy, ruff, pylint, rust (fmt/clippy/test)
4760
├── AGENTS.md # This file
4861
└── README.md # Human-facing documentation
4962
```
5063

64+
### Rust extension (gamecov-core)
65+
66+
The Rust extension is built as part of the package via maturin. The compiled
67+
module is installed as `gamecov._gamecov_core` and provides high-performance
68+
replacements for the BK-tree, union-find, and coverage tracker.
69+
70+
Build the package (includes Rust compilation): `uv sync` or `pip install .`
71+
Run Rust tests independently: `cargo test`
72+
Check Rust formatting: `cargo fmt --all -- --check`
73+
Run Rust linting: `cargo clippy --all-targets --all-features -- -D warnings`
74+
5175
## Design
5276

5377
See [docs/design.md](docs/design.md) for the coverage framework architecture, frame coverage pipeline, BK-tree optimization, and loading strategies.
@@ -59,7 +83,7 @@ See [docs/design.md](docs/design.md) for the coverage framework architecture, fr
5983
| `cov_base.py` | `CoverageItem`, `Coverage[T]`, `CoverageMonitor[T]` protocols/ABC |
6084
| `frame.py` | `Frame` dataclass (PIL Image + average-hash) |
6185
| `dedup.py` | `is_dup()`, `dedup_unique_frames()`, `dedup_unique_hashes()`, `ssim_dedup()` [deprecated] |
62-
| `frame_cov.py` | `FrameCoverage`, `FrameMonitor`, `BKFrameMonitor`, `get_frame_cov()`, `_UnionFind`, `_BKTree` |
86+
| `frame_cov.py` | `FrameCoverage`, `FrameMonitor`, `BKFrameMonitor`, `RustBKFrameMonitor`, `get_frame_cov()`, `_UnionFind`, `_BKTree` |
6387
| `loader.py` | `load_mp4()`, `load_mp4_lazy()`, `load_mp4_last_n()` |
6488
| `writer.py` | `write_mp4()`, `write_mp4_cv2()` |
6589
| `stitch.py` | `stitch_images()` (panorama via AffineStitcher) |
@@ -80,7 +104,8 @@ See [docs/design.md](docs/design.md) for the coverage framework architecture, fr
80104
| `opencv-python` | Color conversion, video writing, image processing |
81105
| `scikit-image` | SSIM metric (deprecated path) |
82106
| `stitching` | Panorama stitching via OpenCV features |
83-
| `numpy`, `numba` | Numerical arrays, optional JIT acceleration |
107+
| `numpy` | Numerical arrays |
108+
| `gamecov._gamecov_core` | Built-in Rust extension: BK-tree, union-find, coverage tracker (PyO3/maturin) |
84109
| `returns` | Functional `Result` type for error handling |
85110
| `deprecated` | `@deprecated` decorator |
86111
| `typer-slim` | CLI framework |
@@ -93,6 +118,7 @@ See [docs/design.md](docs/design.md) for the coverage framework architecture, fr
93118
| `mypy` | Static type checking (strict mode, returns plugin) |
94119
| `ruff` | Linting and formatting |
95120
| `pre-commit` | Pre-commit hook runner |
121+
| `pytest-benchmark` | Performance benchmarking (Python vs Rust) |
96122
| `pytest-xdist` | Parallel test execution (`-n auto`) |
97123
| `pytest-cov` | Coverage reporting |
98124
| `pytest-profiling` | Performance profiling |
@@ -110,6 +136,7 @@ uv run pytest -n auto
110136
- **Integration** (real assets): `test_load_n.py`, `test_load_write_assets.py`
111137
- **Monotonicity**: `test_monotone.py` (random data), `test_monotone_smb.py` (real SMB recordings)
112138
- **Differential**: `test_BK_frame_monitor.py` (FrameMonitor vs BKFrameMonitor produce identical results)
139+
- **Rust backend**: `test_rust_frame_monitor.py` (differential Python vs Rust, order-independence, monotonicity)
113140

114141
Some tests require assets in `assets/videos/` or `assets/smb/` and will skip if missing.
115142

@@ -118,6 +145,24 @@ Some tests require assets in `assets/videos/` or `assets/smb/` and will skip if
118145
- `RADIUS` — Hamming distance threshold (default `5`).
119146
- `N_MAX` — Maximum number of recordings to process in monotonicity tests (default `100`).
120147

148+
## Benchmarks
149+
150+
```bash
151+
# Run benchmarks (disabled by default during normal test runs)
152+
uv run pytest benchmarks/ --benchmark-enable
153+
154+
# Group output by backend for side-by-side comparison
155+
uv run pytest benchmarks/ --benchmark-enable --benchmark-group-by=param:backend
156+
157+
# Save results for later comparison
158+
uv run pytest benchmarks/ --benchmark-enable --benchmark-save=baseline
159+
uv run pytest benchmarks/ --benchmark-enable --benchmark-compare=baseline
160+
```
161+
162+
Benchmarks live in `benchmarks/` and are excluded from the normal test suite.
163+
They compare `BKFrameMonitor` (Python) vs `RustBKFrameMonitor` (Rust) throughput
164+
at the monitor level (`add_cov`/`is_seen` operations).
165+
121166
## Development
122167

123168
- Before start working, refresh your knowledge from contents in `.agents` first.
@@ -129,7 +174,8 @@ Some tests require assets in `assets/videos/` or `assets/smb/` and will skip if
129174
Local variables' types are optional as long as the types can be easily inferred.
130175
- Use f-strings for string interpolation.
131176
- Use `TypedDict`, `Literal`, `Protocol`, and `TypeVar` from `typing` module when appropriate.
132-
- Always run `mypy`, and `ruff` to ensure code quality after updating code in `src/`.
177+
- Always run `mypy` and `ruff` to ensure code quality after updating Python code in `src/`.
178+
- Always run `cargo fmt`, `cargo clippy -- -D warnings`, and `cargo test` after updating Rust code in `src/`.
133179
- Never commit changes or create PRs. Suggest commit messages to the human developer for review after your changes to the codebase.
134180
- Always use `typer` to handle CLI commands.
135181

Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "gamecov-core"
3+
version = "0.2.0"
4+
edition = "2021"
5+
description = "Rust-accelerated core for gamecov frame coverage monitoring"
6+
7+
[lib]
8+
name = "gamecov_core"
9+
crate-type = ["cdylib", "rlib"]
10+
11+
[dependencies]
12+
pyo3 = { version = "0.23", features = ["extension-module"] }
13+
14+
[dev-dependencies]
15+
proptest = "1"
16+
17+
[[test]]
18+
name = "prop_tests"
19+
path = "rust-tests/prop_tests.rs"
20+
21+
[profile.release]
22+
lto = "fat"
23+
codegen-units = 1

README.md

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Two frames are considered duplicates if the Hamming distance between their perce
2525

2626
## Installation
2727

28-
Requires Python >= 3.11.
28+
Requires Python >= 3.11 and a [Rust toolchain](https://rustup.rs/) (for building from source).
2929

3030
### As a package
3131

@@ -41,6 +41,8 @@ Or with pip:
4141
pip install git+https://github.com/SecurityLab-UCD/gamecov.git
4242
```
4343

44+
This builds both the Python package and the embedded Rust extension (`gamecov._gamecov_core`) in a single step.
45+
4446
### For development
4547

4648
Clone the repo and sync dependencies:
@@ -79,6 +81,24 @@ print(f"Total unique frames: {len(monitor.item_seen)}")
7981
print(f"Unique paths: {len(monitor.path_seen)}")
8082
```
8183

84+
### Rust-accelerated monitor
85+
86+
`RustBKFrameMonitor` provides the same interface as `BKFrameMonitor`,
87+
backed by an embedded Rust extension for significantly higher throughput:
88+
89+
```python
90+
from gamecov import FrameCoverage, RustBKFrameMonitor
91+
92+
monitor = RustBKFrameMonitor() # same API as BKFrameMonitor
93+
94+
for recording in recordings:
95+
cov = FrameCoverage(recording)
96+
if not monitor.is_seen(cov):
97+
monitor.add_cov(cov)
98+
99+
print(f"Coverage components: {monitor.coverage_count}")
100+
```
101+
82102
### CLI
83103

84104
```bash
@@ -90,8 +110,12 @@ uv run python src/main.py --input-mp4-path path/to/video.mp4 --confidence-thresh
90110

91111
### Prerequisites
92112

113+
- Python >= 3.11
114+
- [Rust toolchain](https://rustup.rs/) (stable)
115+
- [uv](https://docs.astral.sh/uv/)
116+
93117
```bash
94-
# Install dependencies
118+
# Install dependencies (builds the Rust extension automatically)
95119
uv sync
96120

97121
# Install pre-commit hooks
@@ -101,11 +125,24 @@ uv run pre-commit install
101125
### Running Tests
102126

103127
```bash
104-
# Run all tests in parallel
128+
# Run all Python tests in parallel
105129
uv run pytest -n auto
106130

107131
# Run with coverage
108132
uv run pytest -n auto --cov=gamecov
133+
134+
# Run Rust unit and property tests
135+
cargo test
136+
```
137+
138+
### Benchmarks
139+
140+
```bash
141+
# Compare Python vs Rust monitor throughput
142+
uv run pytest benchmarks/ --benchmark-enable
143+
144+
# Side-by-side grouped by backend
145+
uv run pytest benchmarks/ --benchmark-enable --benchmark-group-by=param:backend
109146
```
110147

111148
### Code Quality
@@ -121,3 +158,29 @@ uv run ruff check src/
121158
### CI
122159

123160
GitHub Actions runs four checks on every PR: `pytest`, `mypy`, `ruff`, and `pylint`.
161+
The CI workflow installs the Rust toolchain before building.
162+
163+
## Performance: Rust vs Python Backend
164+
165+
The embedded Rust extension (`RustBKFrameMonitor`) provides significant speedups
166+
over the pure-Python `BKFrameMonitor` for the core `add_cov`/`is_seen` monitor operations.
167+
The advantage grows with workload size as the BK-tree and union-find structures scale.
168+
169+
Benchmark results (mean time per iteration, lower is better):
170+
171+
| Recordings | Python (ms) | Rust (ms) | Speedup |
172+
| ---------- | ----------- | --------- | ------- |
173+
| 10 | 4.04 | 2.31 | 1.75x |
174+
| 50 | 42.95 | 15.00 | 2.86x |
175+
| 200 | 424.36 | 111.03 | 3.82x |
176+
| 500 | 2,349.74 | 549.40 | 4.28x |
177+
178+
The Rust backend achieves **1.8x -- 4.3x** speedup,
179+
with larger gains at higher workloads where BK-tree traversal and union-find operations dominate.
180+
Each recording contains randomly generated `FrameCoverage` objects with perceptual hashes.
181+
182+
Reproduce these results with:
183+
184+
```bash
185+
uv run pytest benchmarks/ --benchmark-enable --benchmark-group-by=param:backend
186+
```

benchmarks/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)