Skip to content

Commit e90d54b

Browse files
committed
WIP - buffer and flush after block
1 parent 8f3129a commit e90d54b

2 files changed

Lines changed: 168 additions & 145 deletions

File tree

src/crates/primitives/src/indecies.rs

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::fs::{File, OpenOptions};
2-
use std::io;
2+
use std::io::{self, BufWriter, Seek, SeekFrom, Write};
33
use std::os::unix::fs::FileExt;
44
use std::path::Path;
55

@@ -11,9 +11,8 @@ const TXPTR_LEN_BYTES: usize = 28;
1111
const BLOCK_TX_END_LEN_BYTES: usize = 4;
1212
const LINK_LEN_BYTES: usize = 8;
1313

14-
/// Target size of the in-memory append buffer, in bytes. Chosen to amortize
15-
/// the per-write syscall cost without making tail reads (which must scan the
16-
/// buffer) expensive. Each index instance holds at most one such buffer.
14+
/// Capacity of the [`BufWriter`] wrapping each index file. Chosen to amortize
15+
/// the per-write syscall cost. Each index instance holds at most one such buffer.
1716
const APPEND_BUF_CAP_BYTES: usize = 64 * 1024;
1817

1918
pub const OUTID_NONE: u64 = u64::MAX;
@@ -95,22 +94,13 @@ impl TxPtr {
9594

9695
/// Fixed-width append-mostly log of `N`-byte records.
9796
///
98-
/// Appends are staged in an in-memory buffer and flushed to disk in batches
99-
/// via positional writes (`pwrite`), avoiding the per-record `lseek + write`
100-
/// pair that dominates parser IO. Random-access `get`/`set` operations are
101-
/// served from the buffer when they target not-yet-flushed records, so callers
102-
/// that read the tail mid-build (e.g. the parser resolving prevouts against
103-
/// just-appended tx pointers) see a consistent view without explicit flushing.
97+
/// Appends are buffered via [`BufWriter`] and flushed to disk in batches.
98+
/// `get_bytes` serves buffered tail records directly from the writer's buffer.
99+
/// `set_bytes` flushes the buffer first, then uses a positional write so the
100+
/// file cursor is undisturbed for subsequent appends.
104101
#[derive(Debug)]
105102
struct FixedWidthIndex<const N: usize> {
106-
/// Handle used for all positional IO. The file cursor is never read nor
107-
/// modified; we rely on `pread`/`pwrite` via `FileExt`.
108-
file: File,
109-
/// Bytes appended since the last flush. Always a multiple of `N`.
110-
buf: Vec<u8>,
111-
/// Number of records currently persisted to disk.
112-
flushed_len: u64,
113-
/// Total number of records (persisted + buffered). `flushed_len <= len`.
103+
writer: BufWriter<File>,
114104
len: u64,
115105
mmap: Option<Mmap>,
116106
}
@@ -124,16 +114,14 @@ impl<const N: usize> FixedWidthIndex<N> {
124114
.truncate(true)
125115
.open(path)?;
126116
Ok(Self {
127-
file,
128-
buf: Vec::with_capacity(APPEND_BUF_CAP_BYTES),
129-
flushed_len: 0,
117+
writer: BufWriter::with_capacity(APPEND_BUF_CAP_BYTES, file),
130118
len: 0,
131119
mmap: None,
132120
})
133121
}
134122

135123
fn open(path: impl AsRef<Path>, len_error: &'static str) -> io::Result<Self> {
136-
let file = OpenOptions::new().read(true).write(true).open(path)?;
124+
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
137125
let len_bytes = file.metadata()?.len();
138126
if len_bytes % (N as u64) != 0 {
139127
return Err(io::Error::new(io::ErrorKind::InvalidData, len_error));
@@ -145,18 +133,17 @@ impl<const N: usize> FixedWidthIndex<N> {
145133
} else {
146134
None
147135
};
136+
file.seek(SeekFrom::End(0))?;
148137
Ok(Self {
149-
file,
150-
buf: Vec::with_capacity(APPEND_BUF_CAP_BYTES),
151-
flushed_len: len,
138+
writer: BufWriter::with_capacity(APPEND_BUF_CAP_BYTES, file),
152139
len,
153140
mmap,
154141
})
155142
}
156143

157144
/// Open an existing file or create a new one without truncating existing content.
158145
fn open_or_create(path: impl AsRef<Path>, len_error: &'static str) -> io::Result<Self> {
159-
let file = OpenOptions::new()
146+
let mut file = OpenOptions::new()
160147
.read(true)
161148
.write(true)
162149
.truncate(false)
@@ -167,35 +154,26 @@ impl<const N: usize> FixedWidthIndex<N> {
167154
return Err(io::Error::new(io::ErrorKind::InvalidData, len_error));
168155
}
169156
let len = len_bytes / (N as u64);
157+
file.seek(SeekFrom::End(0))?;
170158
Ok(Self {
171-
file,
172-
buf: Vec::with_capacity(APPEND_BUF_CAP_BYTES),
173-
flushed_len: len,
159+
writer: BufWriter::with_capacity(APPEND_BUF_CAP_BYTES, file),
174160
len,
175161
mmap: None,
176162
})
177163
}
178164

179-
/// Flush the in-memory append buffer to disk via a single positional write.
180165
fn flush(&mut self) -> io::Result<()> {
181-
if self.buf.is_empty() {
182-
return Ok(());
183-
}
184-
let offset = self.flushed_len * (N as u64);
185-
self.file.write_all_at(&self.buf, offset)?;
186-
self.flushed_len = self.len;
187-
self.buf.clear();
188-
Ok(())
166+
self.writer.flush()
189167
}
190168

191169
/// Remap the file for read access after all writes are complete.
192170
///
193171
/// Must not be called while any concurrent writes to this file are in flight.
194172
fn remap(&mut self) -> io::Result<()> {
195-
self.flush()?;
173+
self.writer.flush()?;
196174
self.mmap = if self.len > 0 {
197175
// Safety: no more writes will occur on this handle after remap.
198-
Some(unsafe { Mmap::map(&self.file)? })
176+
Some(unsafe { Mmap::map(self.writer.get_ref())? })
199177
} else {
200178
None
201179
};
@@ -211,11 +189,8 @@ impl<const N: usize> FixedWidthIndex<N> {
211189
}
212190

213191
fn append_bytes(&mut self, bytes: &[u8; N]) -> io::Result<u64> {
214-
self.buf.extend_from_slice(bytes);
192+
self.writer.write_all(bytes)?;
215193
self.len += 1;
216-
if self.buf.len() >= APPEND_BUF_CAP_BYTES {
217-
self.flush()?;
218-
}
219194
Ok(self.len - 1)
220195
}
221196

@@ -226,44 +201,34 @@ impl<const N: usize> FixedWidthIndex<N> {
226201
"set_bytes index out of range",
227202
));
228203
}
229-
if index >= self.flushed_len {
230-
let buf_offset = ((index - self.flushed_len) as usize) * N;
231-
self.buf[buf_offset..buf_offset + N].copy_from_slice(bytes);
232-
return Ok(());
233-
}
204+
// Flush so the record is on disk before the positional write.
205+
self.writer.flush()?;
234206
let offset = index * (N as u64);
235-
self.file.write_all_at(bytes, offset)
207+
self.writer.get_ref().write_all_at(bytes, offset)
236208
}
237209

238210
fn get_bytes(&self, index: u64) -> io::Result<Option<[u8; N]>> {
239211
if index >= self.len {
240212
return Ok(None);
241213
}
242-
if index >= self.flushed_len {
243-
let buf_offset = ((index - self.flushed_len) as usize) * N;
214+
let buf = self.writer.buffer();
215+
let flushed_count = self.len - (buf.len() / N) as u64;
216+
if index >= flushed_count {
217+
let buf_offset = ((index - flushed_count) as usize) * N;
244218
let mut out = [0u8; N];
245-
out.copy_from_slice(&self.buf[buf_offset..buf_offset + N]);
219+
out.copy_from_slice(&buf[buf_offset..buf_offset + N]);
246220
return Ok(Some(out));
247221
}
248222
let offset = index * (N as u64);
249223
if let Some(mmap) = &self.mmap {
250224
let offset = offset as usize;
251-
let mut buf = [0u8; N];
252-
buf.copy_from_slice(&mmap[offset..offset + N]);
253-
return Ok(Some(buf));
225+
let mut out = [0u8; N];
226+
out.copy_from_slice(&mmap[offset..offset + N]);
227+
return Ok(Some(out));
254228
}
255-
let mut buf = [0u8; N];
256-
self.file.read_exact_at(&mut buf, offset)?;
257-
Ok(Some(buf))
258-
}
259-
}
260-
261-
impl<const N: usize> Drop for FixedWidthIndex<N> {
262-
fn drop(&mut self) {
263-
// Best-effort flush so that simply dropping an index persists all
264-
// appended records, matching the previous unbuffered behaviour where
265-
// every `append_bytes` landed on disk synchronously.
266-
let _ = self.flush();
229+
let mut out = [0u8; N];
230+
self.writer.get_ref().read_exact_at(&mut out, offset)?;
231+
Ok(Some(out))
267232
}
268233
}
269234

@@ -513,6 +478,17 @@ impl DenseIndexSet {
513478
})
514479
}
515480

481+
/// Flush all buffered appends to disk.
482+
///
483+
/// Must be called before any `set` on recently appended records so those
484+
/// records are on disk for the positional write.
485+
pub fn flush(&mut self) -> io::Result<()> {
486+
self.txptr.inner.flush()?;
487+
self.block_tx.inner.flush()?;
488+
self.in_prevout.inner.flush()?;
489+
self.out_spent.inner.flush()
490+
}
491+
516492
/// Map all four index files into memory for zero-syscall reads.
517493
///
518494
/// Call once after all writes are complete.

0 commit comments

Comments
 (0)