Skip to content

Commit 9dd8221

Browse files
authored
Merge branch 'main' into test-panic-abort
2 parents c804b7b + 4b41b5a commit 9dd8221

12 files changed

Lines changed: 409 additions & 192 deletions

File tree

.github/workflows/benchmark.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Benchmark
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
env:
14+
UV_FROZEN: true
15+
UV_PYTHON: 3.14
16+
RUST_VERSION: "1.90.0"
17+
18+
jobs:
19+
benchmark:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout repository.
24+
uses: actions/checkout@v4
25+
26+
- name: Install UV.
27+
uses: astral-sh/setup-uv@v6
28+
29+
- name: Install dependencies.
30+
run: uv sync --group testing
31+
32+
- name: Compile.
33+
run: uv pip install -v -e .
34+
env:
35+
RUST_BACKTRACE: 1
36+
37+
- name: Run Benchmarks.
38+
run: uv run pytest . -m benchmark_main --benchmark-enable --benchmark-json=output.json
39+
40+
- name: Download previous benchmark data.
41+
uses: actions/cache@v4
42+
with:
43+
path: ./benchmark-data
44+
key: benchmark-${{ github.base_ref || github.ref_name }}
45+
46+
- name: Benchmark results.
47+
uses: benchmark-action/github-action-benchmark@v1
48+
with:
49+
tool: 'pytest'
50+
output-file-path: output.json
51+
external-data-json-path: ./benchmark-data/output.json
52+
summary-always: true
53+
fail-on-alert: true
54+
skip-fetch-gh-pages: true
55+
save-data-file: ${{ github.event_name != 'pull_request' }}

.github/workflows/codspeed.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ permissions:
1212

1313
env:
1414
UV_FROZEN: true
15-
UV_PYTHON: 3.13 # use the latest version of Python because it is faster FIXME(@MarshalX): update to 3.14 when CodSpeed supports it
15+
UV_PYTHON: 3.14
1616
RUST_VERSION: "1.90.0"
1717

1818
jobs:
@@ -79,6 +79,6 @@ jobs:
7979
- name: Run CodSpeed benchmarks.
8080
uses: CodSpeedHQ/action@v4
8181
with:
82-
mode: instrumentation
82+
mode: simulation
8383
token: ${{ secrets.CODSPEED_TOKEN }}
84-
run: uv run --group=codspeed pytest . --codspeed -n auto
84+
run: uv run --group=codspeed pytest . --codspeed

.github/workflows/lint.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Lint and format check
2+
3+
on: [ pull_request ]
4+
5+
permissions:
6+
contents: read
7+
8+
env:
9+
RUSTFLAGS: "-Dwarnings" # treat warnings as errors
10+
11+
jobs:
12+
lint:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Checkout repository.
17+
uses: actions/checkout@v6
18+
19+
- name: Run Clippy.
20+
run: cargo clippy --all-targets --all-features
21+
22+
- name: Run fmt check.
23+
run: cargo fmt --all -- --check

CONTRIBUTING.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
## Contibuting to python-libipld
2+
3+
This project is a small, single-file wrapper around Rust crates like `cid`, `cbor4ii`, and `multibase`, exposing a Python API through `PyO3`. Despite its size, performance matters a lot.
4+
5+
The project uses `uv` package manager. Installing UV: https://docs.astral.sh/uv/getting-started/installation/
6+
7+
Commands for quick start:
8+
```shell
9+
# install deps
10+
uv sync --group all
11+
12+
# compile and install using maturin directly (faster and better for developing)
13+
uv run maturin develop
14+
15+
# compile and install using pip and maturin backend
16+
uv pip install -v -e .
17+
18+
# run all tests
19+
uv run pytest
20+
21+
# run the most important benchmarks
22+
uv run pytest . -m benchmark_main --benchmark-enable
23+
24+
# run lint and fmt
25+
cargo clippy && cargo fmt
26+
```
27+
28+
### Performance
29+
30+
Two key points:
31+
32+
1. Python-side benchmarks
33+
34+
We use `pytest-benchmark` and run all benchmarks from the Python side. `CodSpeed` is used in CI/CD, but it relies on CPU simulation. The best comparison is always on your local machine.
35+
36+
First, capture the baseline from the `main` branch. This records performance relative to your hardware:
37+
```shell
38+
# clone and checkout main branch
39+
uv pip install -v -e .
40+
# run the most important benchmarks
41+
uv run pytest . -m benchmark_main --benchmark-enable --benchmark-save=main
42+
```
43+
44+
Then, on your feature branch, run the same benchmarks but save under a different name (`--benchmark-save` argument)
45+
```shell
46+
# checkout your branch
47+
uv pip install -v -e .
48+
uv run pytest . -m benchmark_main --benchmark-enable --benchmark-save=your_feature
49+
```
50+
51+
Finally, compare results:
52+
```shell
53+
uv run pytest-benchmark compare --group-by="name"
54+
```
55+
56+
Notes:
57+
- Benchmark data is stored under `.benchmarks`.
58+
- You can delete old snapshots during local development.
59+
60+
2. Rust-side benchmarks
61+
62+
We also maintain Rust benchmarks, but they mainly exist for profiling and diagnosing performance issues. They work better with tools like flamegraph than when forced into a Python boundary. See the project's [Makefile](Makefile) for details.
63+
64+
### Testing
65+
66+
All tests target the Python-facing API, which is why the `pytest` directory exists.
67+
68+
Any segfaults or Rust panics **must** be handled safely and must never crash the Python interpreter. Every error must be catchable at the Python layer.
69+
70+
### Style
71+
72+
Use `cargo fmt` and `cargo clippy`. CI will block your PR if formatting or linting fails.
73+
74+
### Things to care about
75+
76+
This library is used in:
77+
- DAG-CBOR benchmarks for Python: https://github.com/DavidBuchanan314/dag-cbor-benchmark
78+
- DASL Testing: https://hyphacoop.github.io/dasl-testing/
79+
80+
Keep these in mind and consider running their test suites against your feature branch locally.

Cargo.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "libipld"
3-
version = "3.1.1"
3+
version = "3.2.0"
44
edition = "2021"
55
license = "MIT"
66
description = "Python binding to the Rust IPLD library"
@@ -16,10 +16,8 @@ crate-type = ["rlib", "cdylib"]
1616
pyo3 = { version = "0.27.1", features = ["generate-import-lib", "anyhow"] }
1717
python3-dll-a = "0.2.14"
1818
anyhow = "1.0.100"
19-
libipld = { version = "0.16.0", features = ["dag-cbor"] }
20-
multibase = "0.9.2"
21-
byteorder = "1.5.0"
22-
multihash = "0.18.1"
19+
cid = "0.11.1"
20+
cbor4ii = { version = "1.2.1" }
2321

2422
[workspace]
2523
members = [ "profiling" ]

profiling/src/profiles/encode_dag_cbor.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ pub fn exec(iterations: u64) {
1414

1515
for _ in 0..iterations {
1616
Python::with_gil(|gil| {
17-
println!("{}", libipld::encode_dag_cbor(gil, &PyString::new(gil, json_str)).is_ok());
17+
println!(
18+
"{}",
19+
libipld::encode_dag_cbor(gil, &PyString::new(gil, json_str)).is_ok()
20+
);
1821
});
1922
}
2023
}

profiling/src/profiles/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
pub mod decode_car;
2-
pub mod encode_dag_cbor;
32
pub mod decode_dag_cbor;
3+
pub mod encode_dag_cbor;

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ testing = [
5050
'pytest-xdist==3.8.0; python_version >= "3.9"',
5151
]
5252
codspeed = [
53-
# only run on CI with the latest Python version FIXME(@MarshalX): update to 3.14 when CodSpeed supports it
54-
'pytest-codspeed==4.1.1; python_version == "3.13" and implementation_name == "cpython"',
53+
# only run on CI with the latest Python version
54+
'pytest-codspeed==4.2.0; python_version == "3.14" and implementation_name == "cpython"',
5555
]
5656

5757
all = [
@@ -60,6 +60,9 @@ all = [
6060
]
6161

6262
[tool.pytest.ini_options]
63+
markers = [
64+
'benchmark_main: marks tests as main benchmarks to run selectively'
65+
]
6366
addopts = [
6467
'--benchmark-columns', 'min,mean,stddev,outliers,rounds,iterations',
6568
'--benchmark-disable', # use --benchmark-enable

pytests/test_dag_cbor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,13 @@ def test_dag_cbor_decode(benchmark, data) -> None:
101101
_dag_cbor_roundtrip(benchmark, data)
102102

103103

104+
@pytest.mark.benchmark_main
104105
@pytest.mark.parametrize('data', load_json_data_fixtures(_REAL_DATA_DIR), ids=lambda data: data[0])
105106
def test_dag_cbor_encode_real_data(benchmark, data) -> None:
106107
_dag_cbor_encode(benchmark, data)
107108

108109

110+
@pytest.mark.benchmark_main
109111
@pytest.mark.parametrize('data', load_json_data_fixtures(_REAL_DATA_DIR), ids=lambda data: data[0])
110112
def test_dag_cbor_decode_real_data(benchmark, data) -> None:
111113
_dag_cbor_roundtrip(benchmark, data)
@@ -116,6 +118,7 @@ def test_dag_cbor_decode_fixtures(benchmark, data) -> None:
116118
_dag_cbor_decode(benchmark, data)
117119

118120

121+
@pytest.mark.benchmark_main
119122
def test_dag_cbor_decode_torture_cids(benchmark) -> None:
120123
dag_cbor = open(_TORTURE_CIDS_DAG_CBOR_PATH, 'rb').read()
121124
benchmark(libipld.decode_dag_cbor, dag_cbor)
@@ -162,7 +165,7 @@ def test_dag_cbor_decode_invalid_utf8() -> None:
162165
libipld.decode_dag_cbor(bytes.fromhex('62c328'))
163166

164167

165-
assert 'Invalid UTF-8 string' in str(exc_info.value)
168+
assert 'utf-8' in str(exc_info.value)
166169

167170

168171
def test_dab_cbor_decode_map_int_key() -> None:

pytests/test_decode_car.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def car() -> bytes:
1414
return load_car_fixture(_DID, _REPO_CAR_PATH)
1515

1616

17+
@pytest.mark.benchmark_main
1718
def test_decode_car(benchmark, car) -> None:
1819
header, blocks = benchmark(libipld.decode_car, car)
1920

0 commit comments

Comments
 (0)