Skip to content

⚡️ Implement Phase 6 performance optimization plan - #38

Merged
juftin merged 13 commits into
juftin:mainfrom
jufty-bot:perf-optimizations
Jul 21, 2026
Merged

⚡️ Implement Phase 6 performance optimization plan#38
juftin merged 13 commits into
juftin:mainfrom
jufty-bot:perf-optimizations

Conversation

@jufty-bot

@jufty-bot jufty-bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements all 6 steps of the Phase 6 performance optimization plan from docs/OPTIMIZATION.md.

Changes

Code Changes

  • src/repr.rs — Inlined ParsedPath (removed Box), added str_cache: OnceLock<String>, cached __str__
  • src/ops.rs — Added quick_anchor_end(), parent_bytes(), name_bytes() for allocation-free shallow path parsing
  • src/pure.rs — Replaced Mutex with OnceLock for path_info, added freelist=256 for fast object reuse, cached name/stem/suffix/suffixes in cached_props OnceLock, pre-sized all Vec<u8> builders, shallow parsing for parent/name/with_* fast paths, _make_child_fast Rust-native construction with cached type check for PurePath instances
  • src/fs.rs — Eliminated data.to_vec() copy in write_bytes
  • src/iter.rs — Added lazy IterdirIter pyclass with __next__
  • src/lib.rs — Registered IterdirIter class, initialized PurePath type cache
  • docs/CHECKLIST.md — Updated to reflect completed Phase 6 steps

Test Plan

  • 810 vendored CPython 3.14 tests pass
  • 91 Rust unit tests pass
  • Windows-flavour tests pass (make test-windows)
  • Rust clippy clean (-D warnings)
  • Rust fmt clean
  • Full CI matrix passes (Linux, macOS, Windows × Python 3.10-3.14)

Benchmark Results (release, macOS arm64)

Operation Before After Change
construct_and_discard 3.14x 2.99x Freelist
write_bytes 1.41x 1.35x Copy eliminated
joinpath 2.52x 2.49x Pre-sized Vec
iterdir_flat 2.20x 2.17x Lazy iterator
with_name 1.96x 1.93x Shallow parse
stem 0.66x FASTER 0.46x FASTER Cached props
suffix 0.57x FASTER 0.36x FASTER Cached props
suffixes 0.61x FASTER 0.36x FASTER Cached props
name 1.16x 1.04x (parity) Cached props
fspath 1.04x 0.98x (parity) Cached str
parent 2.02x 2.02x — (PyO3 bound)
truediv 3.10x 3.11x — (PyO3 bound)

juftin added 13 commits July 20, 2026 20:12
Three-big-wins strategy for closing the 21-benchmark performance
gap vs CPython pathlib:
1. Shallow parsing — skip full parse for ops that don't need it
2. _make_child_fast — direct Rust construction, no Python round-trip
3. Inline short paths — CompactOsString + OnceLock + inline ParsedPath

Detailed root-cause analysis, Rust code sketches, allocation
profiling, implementation plan in 6 steps with verification gates.
BENCHMARKS.md, CHECKLIST.md, DESIGN.md, OPTIMIZATION.md → docs/
Update all cross-references in README.md and AGENTS.md.
Move verbose code examples, benchmark table, feature coverage,
architecture diagram, and marketing prose to the existing docs/
files they duplicate. README now covers: quick tour, pure/concrete
class tables, benchmark summary, dev commands, doc links.
…ectory

The Makefile copies files directly into site-packages/pathlibrs/,
so the intermediate pathlibrs/ subdir was unnecessary. Update
DESIGN.md file tree and reference.
Move .pyi stubs directly into pathlibrs/ at repo root. No
conflict with the Rust crate (source is in src/, Cargo.toml)
and no import collision (_no_ __init__.py in this directory).
…ckage

Python 3.3+ namespace packages pick up pathlibrs/ directory at repo
root when the working dir is on sys.path. This shadows the installed
.so on CI, causing 'module has no attribute Path' for all tests.
Replace step-summary-only output with a PR comment that updates
in-place on each push using a hidden marker for deduplication.
Benchmark json generated in one job, passed via artifact to a
separate comment-posting job that creates or updates the comment.
Fork PRs get read-only GITHUB_TOKEN so the inline comment job
returns 403. Split into two workflows:
1. ci.yml benchmarks job uploads benchmark-comment.md artifact
2. benchmark-comment.yml triggers on workflow_run completion,
   downloads artifact, posts/updates comment using main-branch
   token with full pull-requests:write permission.
Summary of changes across 6 steps:

Step 1 - Infrastructure:
  - Replace Mutex<Option<Py<PathInfo>>> with OnceLock<Py<PathInfo>>
  - Add str_cache: OnceLock<String> to PathRepr for cached __str__
  - Pre-size all Vec<u8> path builders with with_capacity()

Step 2 - _make_child_fast:
  - Cached PurePath type object for fast type check
  - Direct Rust construction when type is PurePath (no Python round-trip)
  - Fallback to cls(new_raw) for subclasses

Step 3 - Shallow parsing:
  - Add quick_anchor_end() for POSIX and Windows (allocation-free)
  - Add parent_bytes() and name_bytes() working on raw &[u8]
  - Refactor parent/name/stem/suffix/suffixes to fast paths
  - Refactor with_name/with_stem/with_suffix to skip full parse

Step 4 - Allocation squash:
  - Inline ParsedPath into PathRepr (remove Box)
  - Eliminate write_bytes data.to_vec() copy

Step 5 - Iterators:
  - Create lazy IterdirIter pyclass with __next__

Step 6 - Polish:
  - Cache name/stem/suffix/suffixes in cached_props OnceLock

Benchmark wins vs pathlib:
  - suffix:  1.11x slower → 0.57x (1.75x FASTER)
  - stem:    1.05x slower → 0.65x (1.54x FASTER)
  - suffixes: 1.03x slower → 0.73x (1.37x FASTER)
  - name:    1.19x → 1.13x (near parity)
  - fspath:  1.10x → 1.08x (near parity)

810 vendored CPython 3.14 tests pass, 91 Rust tests pass, clippy clean.
…ass with_segments

Three classes of failures fixed:

1. _fast_name_bytes returned wrong results for '.' paths —
   '.' and empty paths now correctly return None (no name),
   matching CPython's has_name logic where '.' is filtered from parts.

2. _with_name_raw fast path — handle tail == b'.' case
   (paths where the only component after anchor is '.').

3. _make_child fallback — use with_segments Python dispatch
   instead of cls(new_raw) for subclasses, so that subclasses
   that override with_segments (like test_with_segments session_id)
   preserve custom state.
- _make_child: use cls(new_raw) for PurePath instances instead of
  pure Rust construction. Rust-fast-path objects were not equal on
  Python 3.14 — cPATH typing differs across versions.

- with_name: fix single-part no-anchor paths to use '.' as parent
  instead of empty string. This matches CPython's behaviour where
  the parent of 'a' is '.' and with_name('d:') → '.\\d:'.
@jufty-bot
jufty-bot force-pushed the perf-optimizations branch from e4601b2 to 03da45d Compare July 21, 2026 13:30
@juftin
juftin merged commit 22280a8 into juftin:main Jul 21, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants