Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`BStackChunk<'a>`/`BStackChunkIter<'a>` — fixed-stride view over `BStackSlice` (`alloc`).** `BStackSlice::chunks`/`rchunks` (mirrored on `BStackOwnedSlice`) return `(BStackChunk, BStackSlice)`: aligned view + remainder, pure offset arithmetic, no I/O. `as_slice`/`into_slice`/`with_stride` recover or re-chunk the aligned region. `PartialEq`/`Eq`/`Hash`/`PartialOrd`/`Ord` on `(chunk_len, region)`; no cross-type comparison with `BStackSlice`. Not an iterator itself: `iter()`/`IntoIterator` yield a `BStackChunkIter` (`DoubleEndedIterator` + `ExactSizeIterator` + `FusedIterator`), zero I/O per step.
- **`BStackChunk` search/sort/select.** `binary_search_by`/`binary_search_by_key` (`alloc`): O(log n) chunk reads. `sort_by`/`sort_by_key`/`select_nth_by`/`select_nth_by_key` (`set` + `atomic`): one crash-atomic `BStack::process` call, in-place cycle-following permutation (O(1) scratch chunks, stack-allocated ≤128 B); `select_nth_*` mirrors `[T]::select_nth_unstable_by`.

### Changed

- **`GhostTreeBstackAllocator` — smaller AVL critical section (Rust + C, `alloc`).** `alloc`/`dealloc`/`realloc` of non-tail blocks do less work while holding the allocator mutex. The rebalance up-pass no longer re-reads and re-writes each ancestor through a redundant balance-factor pass — the balance factor and height computed by the node write are threaded into `avl_rebalance` — and each node now caches its two child heights, so the up-pass and rotations write one node per level and read no children in the common in-balance case (down from ~2 writes plus several reads per level). Rust also swaps the per-op heap `Vec` path buffer for a stack array of the fixed `MAX_AVL_DEPTH` bound. Purely internal — no API or observable-behavior change beyond throughput (~25–33% lower per-op latency under real `F_FULLFSYNC`, `benches/alloc.rs`).
- **`GhostTreeBstackAllocator` version bumped to 0.1.3** (`alloc` + `set` features): Magic number updated from `ALGT\x00\x01\x02\x00` to `ALGT\x00\x01\x03\x00`. Reflects the new per-node child-height cache stored in the AVL node header's previously-reserved bytes. Existing 0.1.x files remain fully compatible (only the first 6 bytes are checked on open).

## [0.4.1] - 2026-08-03

### Added
Expand Down
18 changes: 0 additions & 18 deletions PLANNED.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,6 @@ Reference: https://github.com/williamwutq/bstack/pull/37
- **Recovery format.** Whether commit reuses the existing multi-write journal (`wip_aux = MultiWrite`) unchanged — the guard still reduces to a flat non-overlapping `[offset, data]` set — or needs its own `wip_aux` mode.
- **Naming.** `BStackInPlaceGuard`/`inplace_guard()`/`commit()`/`is_inplace_guarded()` are working names.

## GhostTree allocator: multithreaded performance improvement

**Feature flag:** `alloc` (optionally `atomic` for the `Sync` path)
**Breaking change:** No — internal implementation only.

### Motivation

`benches/alloc.rs` result shows `GhostTreeBstackAllocator` is already the fastest general-purpose allocator in the suite, but its scaling under concurrency has received less attention than its single-threaded design: throughput rises from 1t to 4t and then flattens through 16t. This is consistent with `GhostTreeBstackAllocator::lock` — the single mutex serializing all non-tail `alloc`/`dealloc`/`realloc` — capping throughput once contention saturates it. As the crate's best-performing allocator, it is also the one most likely to be used under concurrent load, so its scaling behavior warrants continued performance work, independent of any specific defect.

The mutex's scope and implementation are not in question here (see the `NOT PLANNED` entry on `FirstFitBStackAllocator`'s mutex), and no tree-sharding or other data-structure redesign is intended — that would be a different allocator. The improvement surface is reducing the amount of work done per operation while the mutex is held. Two examples found by code inspection, illustrative rather than exhaustive:

- `avl_insert`, `avl_find_best_fit_and_remove`, and `avl_remove_min` each allocate a `Vec::with_capacity(MAX_AVL_DEPTH)` path buffer under the mutex on every call, despite `MAX_AVL_DEPTH` being a fixed compile-time bound that a stack array could cover instead.
- In the up-pass of `avl_insert` and `avl_find_best_fit_and_remove`, `avl_write_and_update` calls `avl_height` on a child subtree whose height was already computed and written in the previous loop iteration, issuing an avoidable `BStack::get_into` (lock + syscall) to re-fetch it.

### Open questions

- **Validation.** Whether `mixed/uniform` workload is sufficient to measure improvement, or whether a contention-specific microbenchmark (concurrent non-tail alloc/dealloc only) is needed to isolate the critical-section-size effect.

## External-merge-sort strategy and partial sort for `BStackChunk`

**Feature flag:** `alloc` + `set` + `atomic`, same as the `BStackChunk::sort_by`/`select_nth_by` family it extends.
Expand Down
19 changes: 19 additions & 0 deletions c/bstack.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,22 @@ static void win_set_errno(void)
}
}

/* When BSTACK_TEST_NO_DURABLE_SYNC is defined at compile time, plat_durable_sync
* becomes a no-op. This mirrors the Rust crate, whose durable_sync is a no-op in
* cfg(test)+debug builds (src/io_core.rs): an in-process test or fuzz run tears
* the store down logically and reopens it in-process, so skipping the physical
* sync changes neither observable behavior nor the on-disk bytes, yet on macOS
* F_FULLFSYNC otherwise dominates runtime (minutes → seconds). UNSAFE for any
* build that must survive a real crash — test/fuzz builds only, never production. */
static int plat_durable_sync(bstack_fd_t h)
{
#ifdef BSTACK_TEST_NO_DURABLE_SYNC
(void)h;
return 0;
#else
if (!FlushFileBuffers(h)) { win_set_errno(); return -1; }
return 0;
#endif
}

static int plat_file_size(bstack_fd_t h, uint64_t *out)
Expand Down Expand Up @@ -218,14 +230,21 @@ static int plat_ftruncate(bstack_fd_t h, uint64_t size)

#else /* !_WIN32 */

/* No-op under BSTACK_TEST_NO_DURABLE_SYNC — see the note on the Windows
* definition above. Test/fuzz builds only, never production. */
static int plat_durable_sync(bstack_fd_t fd)
{
#ifdef BSTACK_TEST_NO_DURABLE_SYNC
(void)fd;
return 0;
#else
# ifdef __APPLE__
if (fcntl(fd, F_FULLFSYNC) == 0)
return 0;
/* Device does not support F_FULLFSYNC — fall back to fdatasync. */
# endif
return fdatasync(fd);
#endif
}

static int plat_file_size(bstack_fd_t fd, uint64_t *out)
Expand Down
Loading
Loading