Skip to content

Commit 0ba0ff1

Browse files
committed
feat: initial commit
0 parents  commit 0ba0ff1

26 files changed

Lines changed: 1413 additions & 0 deletions

.github/workflows/codspeed.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: CodSpeed
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
benchmarks:
11+
name: Run benchmarks
12+
runs-on: codspeed-macro
13+
permissions:
14+
contents: read
15+
id-token: write
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: 3.15.0-beta.1
23+
architecture: arm64-freethreaded
24+
25+
- name: Install uv
26+
uses: astral-sh/setup-uv@v5
27+
with:
28+
enable-cache: true
29+
30+
- name: Install dependencies
31+
run: uv sync
32+
33+
- name: Hash the dataset seed
34+
# Mix a hash of the secret seed into the cache key so changing the
35+
# seed invalidates the cache, without exposing the seed itself
36+
# (cache keys are visible in the Actions UI).
37+
id: seed-hash
38+
env:
39+
DATASET_SEED: ${{ secrets.DATASET_SEED }}
40+
run: |
41+
hash=$(printf '%s' "$DATASET_SEED" | shasum -a 256 | head -c 16)
42+
echo "value=$hash" >> "$GITHUB_OUTPUT"
43+
44+
- name: Cache datasets
45+
id: cache-datasets
46+
uses: actions/cache@v4
47+
with:
48+
path: |
49+
rounds/1_histogram/data
50+
rounds/2_corruption/data
51+
rounds/3_dna/data
52+
key: datasets-${{ hashFiles('rounds/*/gen_data.py', 'scripts/setup.py') }}-${{ steps.seed-hash.outputs.value }}
53+
54+
- name: Generate datasets
55+
if: steps.cache-datasets.outputs.cache-hit != 'true'
56+
env:
57+
DATASET_SEED: ${{ secrets.DATASET_SEED }}
58+
run: uv run scripts/setup.py ${DATASET_SEED:+--seed "$DATASET_SEED"}
59+
60+
- name: Run correctness tests
61+
run: uv run pytest -k "not test_bench"
62+
63+
- name: Run benchmarks
64+
uses: CodSpeedHQ/action@v4
65+
with:
66+
mode: walltime
67+
run: uv run pytest --codspeed

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CFP.md
2+
3+
# Workshop datasets — generated locally via scripts/setup.py
4+
rounds/*/data/
5+
6+
# Instructor-only reference implementations
7+
solutions/
8+
9+
# Python
10+
__pycache__/
11+
*.py[cod]
12+
.pytest_cache/
13+
.venv/
14+
15+
# CodSpeed
16+
.codspeed/

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.15t

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Python Performance Lab: Sharpening Your Instincts
2+
3+
A PyCon US 2026 hands-on tutorial. You optimize intentionally slow Python code
4+
across three rounds plus a team challenge, measuring every change with
5+
[CodSpeed](https://codspeed.io).
6+
7+
## Rounds
8+
9+
| Round | Topic | Skills |
10+
| -------------------------- | -------------------- | ------------------------------------- |
11+
| [1](rounds/1_histogram/) | Byte-pair histogram | Data representation, vectorization |
12+
| [2](rounds/2_corruption/) | Corruption scanner | Vectorization, parallelism |
13+
| [3](rounds/3_dna/) (final) | DNA sequence matcher | Everything above, as a team challenge |
14+
15+
Each round ships an intentionally slow `baseline.py` (a read-only reference),
16+
a `solution.py` you edit, deterministic data generators, parametrized
17+
correctness tests, and benchmarks that run baseline and solution
18+
side-by-side.
19+
20+
## Setup
21+
22+
You need [`uv`](https://docs.astral.sh/uv/). Python 3.15t will be downloaded directly.
23+
24+
The order below matters: forking, logging in, and doing the first run on `main`
25+
register you on the live leaderboard, so every later push to your branch shows
26+
up as a side-by-side comparison against your own baseline.
27+
28+
```bash
29+
# 1. Fork github.com/CodSpeedHQ/pyconus-2026-tutorial, then clone your fork.
30+
git clone https://github.com/<you>/pyconus-2026-tutorial && cd pyconus-2026-tutorial
31+
32+
# 2. Install deps + generate the datasets (~650 MB total).
33+
uv sync
34+
uv run scripts/setup.py
35+
36+
# 3. Install the CodSpeed CLI and log in.
37+
curl -L https://codspeed.io/install.sh | sh
38+
codspeed auth login
39+
40+
# 4. Branch off. Every push to this branch re-runs and re-ranks you.
41+
git checkout -b <your-name>
42+
```
43+
44+
Generate smaller datasets on lower-spec machines:
45+
46+
```bash
47+
uv run scripts/setup.py --round1-mb 10 --round2-mb 32 --round3-mb 100
48+
```
49+
50+
## Working on a round
51+
52+
Every round directory ships its own `README.md`. The commands are the same
53+
shape every time, illustrated here for Round 1:
54+
55+
```bash
56+
# Correctness tests against the small fixture.
57+
uv run pytest rounds/1_histogram/
58+
59+
# Walltime benchmark against the full dataset.
60+
uv run pytest --codspeed rounds/1_histogram/
61+
62+
# Same, run through the CodSpeed CLI with the walltime mode
63+
codspeed run --mode walltime -- uv run pytest --codspeed rounds/1_histogram/
64+
```
65+
66+
Edit `solution.py` to optimize. Leave `baseline.py` alone so the side-by-side
67+
comparison stays meaningful. Every test and benchmark is parametrized over
68+
both implementations, so the output always shows `[baseline]` versus
69+
`[solution]`.
70+
71+
## Layout
72+
73+
```
74+
rounds/
75+
1_histogram/ # baseline.py, solution.py, gen_data.py, tests.
76+
2_corruption/
77+
3_dna/
78+
scripts/
79+
setup.py # one-shot data generation across every round.
80+
```
81+
82+
Each round's `data/` directory is generated locally and gitignored.

pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[project]
2+
name = "pyconus-2026-tutorial"
3+
version = "0.1.0"
4+
description = "Python Performance Lab: Sharpening Your Instincts — PyCon US 2026 tutorial"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = ["numpy>=2.0"]
8+
9+
[dependency-groups]
10+
dev = ["pytest>=8.0", "pytest-codspeed>=5.0.1"]
11+
12+
[tool.pytest.ini_options]
13+
testpaths = ["rounds"]
14+
# importlib avoids module-name collisions across the round directories
15+
# (every round has its own baseline.py).
16+
addopts = "--import-mode=importlib"

rounds/1_histogram/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Round 1: Byte-pair histogram
2+
3+
## Problem
4+
5+
Given a binary payload of up to a few hundred megabytes, count the frequency
6+
of every **2-byte bigram**. The output is a mapping from each observed bigram
7+
to its occurrence count.
8+
9+
A **bigram** is a sliding window of two adjacent bytes. For the payload
10+
`b"ABCD"` the bigrams are `b"AB"`, `b"BC"`, and `b"CD"`. An `N`-byte payload
11+
therefore contains `N - 1` bigrams. With 256 possible byte values each, the
12+
full universe is `256 * 256 = 65,536` distinct tokens.
13+
14+
- Input: `data/payload.bin` (default 10 MB, biased byte distribution).
15+
- Output: a `dict` (or equivalent) keyed by 2-byte token, mapped to an `int` count.
16+
- Universe: up to 65,536 distinct bigrams.
17+
18+
## Files
19+
20+
| File | Purpose |
21+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
22+
| `baseline.py` | Intentionally slow starting point. **Don't edit:** it is the reference for the comparison. |
23+
| `solution.py` | **Edit this.** Starts out delegating to `baseline.py`; replace with your faster implementation. |
24+
| `gen_data.py` | Generates `data/payload.bin` and `data/fixture/payload.bin`. |
25+
| `test_histogram.py` | Correctness tests and the pytest-codspeed benchmark. Every test is parametrized over both the baseline and your solution. |
26+
27+
## Generate the data
28+
29+
```bash
30+
uv run rounds/1_histogram/gen_data.py # default 10 MB.
31+
uv run rounds/1_histogram/gen_data.py --size-mb 50
32+
```
33+
34+
Or run `uv run scripts/setup.py` to generate every round's data in
35+
one go.
36+
37+
## Verify correctness
38+
39+
```bash
40+
uv run pytest rounds/1_histogram/
41+
```
42+
43+
## Benchmark
44+
45+
Walltime, locally:
46+
47+
```bash
48+
uv run pytest --codspeed rounds/1_histogram/
49+
```
50+
51+
Same benchmarks, run through the CodSpeed CLI for low-noise instrumented
52+
measurements:
53+
54+
```bash
55+
codspeed run --mode walltime -- uv run pytest --codspeed rounds/1_histogram/
56+
```

rounds/1_histogram/__init__.py

Whitespace-only changes.

rounds/1_histogram/baseline.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Round 1 baseline: byte-pair histogram.
2+
3+
Counts the frequency of every 2-byte bigram (256 * 256 = 65,536 possible
4+
tokens) in a binary payload.
5+
"""
6+
7+
8+
def compute_histogram(path: str) -> dict[bytes, int]:
9+
"""Frequency of every 2-byte bigram in the file at ``path``."""
10+
# Step 1: read the whole file into memory as a single bytes object.
11+
with open(path, "rb") as f:
12+
data = f.read()
13+
14+
# Step 2: slide a 2-byte window across the buffer. For ``b"ABCD"`` the
15+
# iterations produce ``b"AB"``, ``b"BC"``, then ``b"CD"``. For each window,
16+
# bump the matching bucket in a ``dict`` keyed by the bigram itself.
17+
counts: dict[bytes, int] = {}
18+
for i in range(len(data) - 1):
19+
bigram = data[i : i + 2]
20+
if bigram in counts:
21+
counts[bigram] += 1
22+
else:
23+
counts[bigram] = 1
24+
return counts

rounds/1_histogram/gen_data.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Generate the Round 1 dataset: a binary payload with biased byte frequencies.
2+
3+
Run from anywhere:
4+
5+
uv run rounds/1_histogram/gen_data.py # default 10 MB
6+
uv run rounds/1_histogram/gen_data.py --size-mb 50
7+
8+
Output:
9+
rounds/1_histogram/data/payload.bin — full benchmark dataset
10+
rounds/1_histogram/data/fixture_payload.bin — tiny fixture for tests
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import argparse
16+
import os
17+
import random
18+
from pathlib import Path
19+
20+
DATA_DIR = Path(__file__).parent / "data"
21+
22+
FIXTURE_SIZE_BYTES = 64 * 1024 # 64 KB fixture — fast, deterministic
23+
24+
25+
def _biased_alphabet() -> bytes:
26+
"""Skew byte frequencies so the histogram is non-trivial.
27+
28+
A flat-random payload makes every bucket land near the mean, which hides
29+
bugs in attendee implementations. We instead bias toward a smaller alphabet
30+
with realistic-looking long tails.
31+
"""
32+
common = b"ETAOINSHRDLU " # frequent ASCII letters + space
33+
medium = b"abcdefghijklmnopqrstuvwxyz0123456789\n"
34+
rare = bytes(range(256))
35+
return common * 40 + medium * 8 + rare
36+
37+
38+
def _write_payload(path: Path, size_bytes: int, seed: int) -> None:
39+
rng = random.Random(seed)
40+
alphabet = _biased_alphabet()
41+
chunk_size = 1 << 20 # 1 MB at a time keeps peak memory low
42+
remaining = size_bytes
43+
with path.open("wb") as f:
44+
while remaining > 0:
45+
n = min(chunk_size, remaining)
46+
f.write(bytes(rng.choices(alphabet, k=n)))
47+
remaining -= n
48+
49+
50+
def main() -> None:
51+
parser = argparse.ArgumentParser(description=__doc__)
52+
parser.add_argument(
53+
"--size-mb",
54+
type=int,
55+
default=10,
56+
help="Size of the full benchmark payload in MB (default: 10).",
57+
)
58+
parser.add_argument(
59+
"--seed",
60+
type=int,
61+
default=42,
62+
help="Random seed for deterministic output (default: 42).",
63+
)
64+
args = parser.parse_args()
65+
66+
DATA_DIR.mkdir(parents=True, exist_ok=True)
67+
68+
full_path = DATA_DIR / "payload.bin"
69+
fixture_path = DATA_DIR / "fixture_payload.bin"
70+
71+
print(f"writing fixture: {fixture_path} ({FIXTURE_SIZE_BYTES} bytes)")
72+
_write_payload(fixture_path, FIXTURE_SIZE_BYTES, seed=args.seed + 1)
73+
74+
full_size = args.size_mb * 1024 * 1024
75+
print(f"writing payload: {full_path} ({args.size_mb} MB)")
76+
_write_payload(full_path, full_size, seed=args.seed)
77+
78+
print(f"done. total on disk: {os.path.getsize(full_path) + os.path.getsize(fixture_path):,} bytes")
79+
80+
81+
if __name__ == "__main__":
82+
main()

rounds/1_histogram/solution.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Your Round 1 solution — byte-pair histogram.
2+
3+
**Edit this file.** It currently delegates to ``baseline.py`` so everything
4+
passes out of the box. Replace the body of ``compute_histogram`` with your
5+
own faster implementation.
6+
"""
7+
8+
9+
def compute_histogram(path: str) -> dict[bytes, int]:
10+
"""Frequency of every 2-byte bigram in the file at ``path``."""
11+
# TODO: remove this delegation and write your own implementation here.
12+
from .baseline import compute_histogram as _baseline
13+
14+
return _baseline(path)

0 commit comments

Comments
 (0)