1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! Resize and reallocation logic for [`ContiguousVectors`].
//!
//! Extracted from [`super::perf_optimizations`] to isolate the allocation-growth
//! concern from the core storage API. Uses [`AllocGuard`] for panic-safe buffer
//! migration during capacity changes.
//!
//! [`AllocGuard`]: crate::alloc_guard::AllocGuard
use std::alloc::dealloc;
use std::ptr::{self, NonNull};
use super::perf_optimizations::ContiguousVectors;
impl ContiguousVectors {
/// Ensures the storage has capacity for at least `required_capacity` vectors.
///
/// # Errors
///
/// Returns [`Error::AllocationFailed`] if reallocation fails.
///
/// [`Error::AllocationFailed`]: crate::error::Error::AllocationFailed
pub fn ensure_capacity(&mut self, required_capacity: usize) -> crate::error::Result<()> {
if required_capacity > self.capacity {
// #899: `self.capacity * 2` could overflow `usize` and wrap to a tiny
// value, defeating the `.max(required_capacity)` guard. Saturate the
// doubling so growth always satisfies `required_capacity` and the
// final layout-size check (in `resize`) still rejects true overflow.
let grown = if self.backing.is_heap() {
self.capacity.saturating_mul(2)
} else {
// A file-backed arena IS the durable `.vectors` file (#2173):
// its slack is disk space `allocate` really reserves, and no
// dump gives it back. Doubling left a reopened collection's
// file at up to twice its payload after one insert. An eighth
// bounds the slack to 12.5% while growth stays geometric, so
// a push is still amortised O(1) — and no step copies: the
// mapping just covers more of the same file.
self.capacity.saturating_add(self.capacity / 8)
};
let new_capacity = required_capacity.max(grown);
self.resize(new_capacity)?;
}
Ok(())
}
/// Pre-allocates capacity for `additional` more vectors beyond the current length.
///
/// Analogous to [`Vec::reserve`]: ensures the buffer can hold
/// `self.len() + additional` vectors without reallocating. No-op if
/// sufficient capacity already exists.
///
/// Call before a batch push to guarantee `push_batch` won't resize.
///
/// # Errors
///
/// Returns [`Error::AllocationFailed`] if reallocation fails.
///
/// [`Error::AllocationFailed`]: crate::error::Error::AllocationFailed
pub fn reserve_additional(&mut self, additional: usize) -> crate::error::Result<()> {
let required = self.count.saturating_add(additional);
self.ensure_capacity(required)
}
/// Resizes the internal buffer.
///
/// # P2 Audit + PERF-002: Panic-Safety with RAII Guard
///
/// This function uses `AllocGuard` for panic-safe allocation:
/// 1. New buffer is allocated via RAII guard (auto-freed on panic)
/// 2. Data is copied to new buffer
/// 3. Guard ownership is transferred (no auto-free)
/// 4. Old buffer is deallocated
/// 5. State is updated atomically
///
/// If panic occurs during copy, the guard ensures new buffer is freed.
pub(crate) fn resize(&mut self, new_capacity: usize) -> crate::error::Result<()> {
if new_capacity <= self.capacity {
return Ok(());
}
// Matching here rather than asking `is_heap` and re-destructuring in a
// helper: this way the mapped branch *has* the arena, so there is no
// "called on a heap-backed arena" state left to report at runtime.
#[cfg(feature = "persistence")]
if let crate::perf_optimizations::ArenaBacking::FileMapped(ref mut arena) = self.backing {
let byte_len = Self::byte_size(self.dimension, new_capacity)?;
self.data = Self::grow_mapped(arena, byte_len)?;
self.capacity = new_capacity;
return Ok(());
}
let old_layout = Self::layout(self.dimension, self.capacity)?;
let new_layout = Self::layout(self.dimension, new_capacity)?;
let new_data = Self::alloc_and_copy(new_layout, self.data, self.count, self.dimension)?;
// Deallocate old buffer
// SAFETY: self.data was allocated with old_layout, is non-null (NonNull invariant)
// - Condition 1: old_layout matches the allocation parameters.
// - Condition 2: Pointer is non-null per NonNull invariant.
// SAFETY: Free old buffer after data migration to new buffer.
unsafe {
dealloc(self.data.as_ptr().cast::<u8>(), old_layout);
}
// Update state (all-or-nothing)
self.data = new_data;
self.capacity = new_capacity;
Ok(())
}
/// Grows a mapped arena's file and returns the pointer to its data.
///
/// No copy happens: the bytes already written keep their place on disk and
/// the kernel hands back a mapping that covers more of the same file. The
/// heap path cannot do this — it must `memcpy` the whole arena into a
/// larger block — so growth is strictly cheaper here.
///
/// The freshly appended range reads as zeros, matching the `alloc_zeroed`
/// guarantee `insert_at` depends on when it leaves gaps.
///
/// Takes the arena rather than `&mut self` so the heap case is not
/// representable: there is no branch here to get wrong, and the returned
/// pointer is the only thing the caller has to install. Growing re-maps,
/// so the caller's old address is stale and this value replaces it.
///
/// # Errors
///
/// Returns [`Error::AllocationFailed`] if the file cannot be extended or
/// re-mapped. The arena is left untouched and usable in that case.
///
/// [`Error::AllocationFailed`]: crate::error::Error::AllocationFailed
#[cfg(feature = "persistence")]
fn grow_mapped(
arena: &mut crate::contiguous_file_arena::FileArena,
byte_len: usize,
) -> crate::error::Result<std::ptr::NonNull<f32>> {
arena.grow(byte_len).map_err(|e| {
crate::error::Error::AllocationFailed(format!(
"failed to grow the file-backed vector arena to {byte_len} bytes: {e}"
))
})?;
Ok(arena.data_ptr())
}
/// Allocates a new buffer and copies existing data into it.
///
/// Uses `AllocGuard` for panic-safety: if copy panics, the guard drops
/// and frees the new buffer automatically.
#[allow(clippy::cast_ptr_alignment)] // Layout is 64-byte aligned
fn alloc_and_copy(
new_layout: std::alloc::Layout,
src: NonNull<f32>,
count: usize,
dimension: usize,
) -> crate::error::Result<NonNull<f32>> {
use crate::alloc_guard::AllocGuard;
// Allocate zero-initialized buffer with RAII guard (PERF-002)
let guard = AllocGuard::new_zeroed(new_layout).ok_or_else(|| {
crate::error::Error::AllocationFailed(format!(
"Failed to allocate {} bytes for ContiguousVectors resize",
new_layout.size()
))
})?;
// EPIC-032/US-002: Use NonNull for type-level guarantee
let new_data = NonNull::new(guard.cast::<f32>()).ok_or_else(|| {
crate::error::Error::AllocationFailed("AllocGuard returned null pointer".to_string())
})?;
// Copy existing data to new buffer
if count > 0 {
let copy_size = count * dimension;
// SAFETY: Both pointers are valid (NonNull), non-overlapping, and properly aligned
// - Condition 1: Source pointer (src) is valid and properly aligned.
// - Condition 2: Destination pointer (new_data) is valid and properly aligned.
// - Condition 3: Pointers are non-overlapping (old and new allocations are distinct).
// SAFETY: Migrate data to newly allocated buffer during resize.
unsafe {
ptr::copy_nonoverlapping(src.as_ptr(), new_data.as_ptr(), copy_size);
}
}
// Transfer ownership - guard won't free on drop anymore
let _ = guard.into_raw();
Ok(new_data)
}
}