v8: transparent lz4 compression

This commit is contained in:
Jeremy Soller
2025-07-31 18:48:17 -06:00
parent 0cb6f3cad5
commit 26dc32d710
8 changed files with 186 additions and 59 deletions
Generated
+7
View File
@@ -318,6 +318,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lz4_flex"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
[[package]]
name = "memchr"
version = "2.7.5"
@@ -454,6 +460,7 @@ dependencies = [
"libc",
"libredox",
"log",
"lz4_flex",
"range-tree",
"redox-path",
"redox-scheme",
+1
View File
@@ -40,6 +40,7 @@ getrandom = { version = "0.2.5", optional = true }
libc = "0.2"
libredox = { version = "0.1.6", features = ["call"], optional = true }
log = { version = "0.4.14", default-features = false, optional = true }
lz4_flex = { version = "0.11", default-features = false, features = ["checked-decode"] }
range-tree = { version = "0.1", optional = true }
redox-path = "0.3.0"
redox-scheme = { version = "0.7.0", optional = true }
+6 -6
View File
@@ -2,7 +2,7 @@ use alloc::vec::Vec;
use core::{fmt, mem, ops, slice};
use endian_num::Le;
use crate::{BlockAddr, BlockLevel, BlockPtr, BlockTrait, BLOCK_SIZE};
use crate::{BlockAddr, BlockLevel, BlockMeta, BlockPtr, BlockTrait, BLOCK_SIZE};
pub const ALLOC_LIST_ENTRIES: usize =
(BLOCK_SIZE as usize - mem::size_of::<BlockPtr<AllocList>>()) / mem::size_of::<AllocEntry>();
@@ -52,10 +52,10 @@ impl Allocator {
/// Find a free block of the given level, mark it as "used", and return its address.
/// Returns [`None`] if there are no free blocks with this level.
pub fn allocate(&mut self, block_level: BlockLevel) -> Option<BlockAddr> {
pub fn allocate(&mut self, meta: BlockMeta) -> Option<BlockAddr> {
// First, find the lowest level with a free block
let mut index_opt = None;
let mut level = block_level.0;
let mut level = meta.level.0;
// Start searching at the level we want. Smaller levels are too small!
while level < self.levels.len() {
if !self.levels[level].is_empty() {
@@ -68,13 +68,13 @@ impl Allocator {
// If a free block was found, split it until we find a usable block of the right level.
// The left side of the split block is kept free, and the right side is allocated.
let index = index_opt?;
while level > block_level.0 {
while level > meta.level.0 {
level -= 1;
let level_size = 1 << level;
self.levels[level].push(index + level_size);
}
Some(unsafe { BlockAddr::new(index, block_level) })
Some(unsafe { BlockAddr::new(index, meta) })
}
/// Try to allocate the exact block specified, making all necessary splits.
@@ -112,7 +112,7 @@ impl Allocator {
}
}
Some(unsafe { BlockAddr::new(index_opt?, exact_addr.level()) })
Some(unsafe { BlockAddr::new(index_opt?, exact_addr.meta()) })
}
/// Deallocate the given block, marking it "free" so that it can be re-used later.
+65 -14
View File
@@ -9,38 +9,67 @@ const BLOCK_LIST_ENTRIES: usize = BLOCK_SIZE as usize / mem::size_of::<BlockPtr<
///
/// This encodes a block's position _and_ [`BlockLevel`]:
/// the first four bits of this `u64` encode the block's level,
/// the next four bits indicates decompression level,
/// the rest encode its index.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct BlockAddr(u64);
impl BlockAddr {
const INDEX_SHIFT: u64 = 8;
const DECOMP_LEVEL_MASK: u64 = 0xF0;
const DECOMP_LEVEL_SHIFT: u64 = 4;
const LEVEL_MASK: u64 = 0xF;
// Unsafe because this can create invalid blocks
pub(crate) unsafe fn new(index: u64, level: BlockLevel) -> Self {
// Level must only use the lowest four bits
if level.0 > 0xF {
panic!("block level used more than four bits");
pub(crate) unsafe fn new(index: u64, meta: BlockMeta) -> Self {
// Level must fit within LEVEL_MASK
if meta.level.0 > Self::LEVEL_MASK as usize {
panic!("block level too large");
}
// Index must not use the highest four bits
// Decomp level must fit within DECOMP_LEVEL_MASK
let decomp_level = meta.decomp_level.unwrap_or_default();
if (decomp_level.0 << Self::DECOMP_LEVEL_SHIFT) > Self::DECOMP_LEVEL_MASK as usize {
panic!("decompressed block level too large");
}
// Index must not use the metadata bits
let inner = index
.checked_shl(4)
.expect("block index used highest four bits")
| (level.0 as u64);
.checked_shl(Self::INDEX_SHIFT as u32)
.expect("block index too large")
| ((decomp_level.0 as u64) << Self::DECOMP_LEVEL_SHIFT)
| (meta.level.0 as u64);
Self(inner)
}
pub fn null(level: BlockLevel) -> Self {
unsafe { Self::new(0, level) }
pub fn null(meta: BlockMeta) -> Self {
unsafe { Self::new(0, meta) }
}
pub fn index(&self) -> u64 {
// The first four bits store the level
self.0 >> 4
self.0 >> Self::INDEX_SHIFT
}
pub fn level(&self) -> BlockLevel {
// The first four bits store the level
BlockLevel((self.0 & 0xF) as usize)
BlockLevel((self.0 & Self::LEVEL_MASK) as usize)
}
pub fn decomp_level(&self) -> Option<BlockLevel> {
let value = (self.0 & Self::DECOMP_LEVEL_MASK) >> Self::DECOMP_LEVEL_SHIFT;
if value != 0 {
Some(BlockLevel(value as usize))
} else {
None
}
}
pub fn meta(&self) -> BlockMeta {
BlockMeta {
level: self.level(),
decomp_level: self.decomp_level(),
}
}
pub fn is_null(&self) -> bool {
@@ -48,6 +77,28 @@ impl BlockAddr {
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct BlockMeta {
pub(crate) level: BlockLevel,
pub(crate) decomp_level: Option<BlockLevel>,
}
impl BlockMeta {
pub fn new(level: BlockLevel) -> Self {
Self {
level,
decomp_level: None,
}
}
pub fn new_compressed(level: BlockLevel, decomp_level: BlockLevel) -> Self {
Self {
level,
decomp_level: Some(decomp_level),
}
}
}
/// The size of a block.
///
/// Level 0 blocks are blocks of [`BLOCK_SIZE`] bytes.
@@ -211,9 +262,9 @@ pub struct BlockPtr<T> {
}
impl<T> BlockPtr<T> {
pub fn null(level: BlockLevel) -> Self {
pub fn null(meta: BlockMeta) -> Self {
Self {
addr: BlockAddr::null(level).0.into(),
addr: BlockAddr::null(meta).0.into(),
hash: 0.into(),
phantom: PhantomData,
}
+8 -6
View File
@@ -5,7 +5,9 @@ use xts_mode::{get_tweak_default, Xts128};
#[cfg(feature = "std")]
use crate::{AllocEntry, AllocList, BlockData, BlockTrait, Key, KeySlot, Node, Salt, TreeList};
use crate::{Allocator, BlockAddr, BlockLevel, Disk, Header, Transaction, BLOCK_SIZE, HEADER_RING};
use crate::{
Allocator, BlockAddr, BlockLevel, BlockMeta, Disk, Header, Transaction, BLOCK_SIZE, HEADER_RING,
};
/// A file system
pub struct FileSystem<D: Disk> {
@@ -174,12 +176,12 @@ impl<D: Disk> FileSystem<D> {
// Set tree and alloc pointers and write header generation one
fs.tx(|tx| unsafe {
let tree = BlockData::new(
BlockAddr::new(HEADER_RING + 1, BlockLevel::default()),
BlockAddr::new(HEADER_RING + 1, BlockMeta::default()),
TreeList::empty(BlockLevel::default()).unwrap(),
);
let mut alloc = BlockData::new(
BlockAddr::new(HEADER_RING + 2, BlockLevel::default()),
BlockAddr::new(HEADER_RING + 2, BlockMeta::default()),
AllocList::empty(BlockLevel::default()).unwrap(),
);
@@ -199,7 +201,7 @@ impl<D: Disk> FileSystem<D> {
fs.tx(|tx| unsafe {
let mut root = BlockData::new(
BlockAddr::new(HEADER_RING + 3, BlockLevel::default()),
BlockAddr::new(HEADER_RING + 3, BlockMeta::default()),
Node::new(Node::MODE_DIR | 0o755, 0, 0, ctime, ctime_nsec),
);
root.data_mut().set_links(1);
@@ -254,12 +256,12 @@ impl<D: Disk> FileSystem<D> {
if count < 0 {
for i in 0..-count {
//TODO: replace assert with error?
let addr = BlockAddr::new(index + i as u64, BlockLevel::default());
let addr = BlockAddr::new(index + i as u64, BlockMeta::default());
assert_eq!(self.allocator.allocate_exact(addr), Some(addr));
}
} else {
for i in 0..count {
let addr = BlockAddr::new(index + i as u64, BlockLevel::default());
let addr = BlockAddr::new(index + i as u64, BlockMeta::default());
self.allocator.deallocate(addr);
}
}
+2 -2
View File
@@ -16,7 +16,7 @@ pub const BLOCK_SIZE: u64 = 4096;
pub const RECORD_LEVEL: usize = 5;
pub const RECORD_SIZE: u64 = BLOCK_SIZE << RECORD_LEVEL;
pub const SIGNATURE: &[u8; 8] = b"RedoxFS\0";
pub const VERSION: u64 = 7;
pub const VERSION: u64 = 8;
pub const DIR_ENTRY_MAX_LENGTH: usize = 252;
pub static IS_UMT: AtomicUsize = AtomicUsize::new(0);
@@ -25,7 +25,7 @@ pub use self::allocator::{AllocEntry, AllocList, Allocator, ALLOC_LIST_ENTRIES};
#[cfg(feature = "std")]
pub use self::archive::{archive, archive_at};
pub use self::block::{
BlockAddr, BlockData, BlockLevel, BlockList, BlockPtr, BlockRaw, BlockTrait,
BlockAddr, BlockData, BlockLevel, BlockList, BlockMeta, BlockPtr, BlockRaw, BlockTrait,
};
pub use self::dir::{DirEntry, DirList};
pub use self::disk::*;
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::{BlockLevel, BlockTrait, RECORD_LEVEL};
//TODO: this is a box to prevent stack overflows
#[derive(Clone)]
pub struct RecordRaw(Box<[u8]>);
pub struct RecordRaw(pub(crate) Box<[u8]>);
unsafe impl BlockTrait for RecordRaw {
fn empty(level: BlockLevel) -> Option<Self> {
+96 -30
View File
@@ -14,9 +14,9 @@ use syscall::error::{
use crate::{
htree::{self, HTreeHash, HTreeNode, HTreePtr},
AllocEntry, AllocList, Allocator, BlockAddr, BlockData, BlockLevel, BlockPtr, BlockTrait,
DirEntry, DirList, Disk, FileSystem, Header, Node, NodeLevel, RecordRaw, TreeData, TreePtr,
ALLOC_GC_THRESHOLD, ALLOC_LIST_ENTRIES, DIR_ENTRY_MAX_LENGTH, HEADER_RING,
AllocEntry, AllocList, Allocator, BlockAddr, BlockData, BlockLevel, BlockMeta, BlockPtr,
BlockTrait, DirEntry, DirList, Disk, FileSystem, Header, Node, NodeLevel, RecordRaw, TreeData,
TreePtr, ALLOC_GC_THRESHOLD, ALLOC_LIST_ENTRIES, DIR_ENTRY_MAX_LENGTH, HEADER_RING,
};
pub struct Transaction<'a, D: Disk> {
@@ -57,11 +57,11 @@ impl<'a, D: Disk> Transaction<'a, D> {
// MARK: block operations
//
/// Allocate a new block of size `level`, returning its address.
/// Allocate a new block of size defined by `meta`, returning its address.
/// - returns `Err(ENOSPC)` if a block of this size could not be alloated.
/// - unsafe because order must be done carefully and changes must be flushed to disk
pub(crate) unsafe fn allocate(&mut self, level: BlockLevel) -> Result<BlockAddr> {
match self.allocator.allocate(level) {
pub(crate) unsafe fn allocate(&mut self, meta: BlockMeta) -> Result<BlockAddr> {
match self.allocator.allocate(meta) {
Some(addr) => {
self.allocator_log.push_back(AllocEntry::allocate(addr));
Ok(addr)
@@ -180,7 +180,7 @@ impl<'a, D: Disk> Transaction<'a, D> {
while new_blocks.len() * ALLOC_LIST_ENTRIES
<= self.allocator_log.len() + self.deallocate.len()
{
new_blocks.push(unsafe { self.allocate(BlockLevel::default())? });
new_blocks.push(unsafe { self.allocate(BlockMeta::default())? });
}
// De-allocate old blocks (after allocation to prevent re-use)
@@ -321,8 +321,9 @@ impl<'a, D: Disk> Transaction<'a, D> {
ptr: BlockPtr<T>,
) -> Result<BlockData<T>> {
if ptr.is_null() {
match T::empty(ptr.addr().level()) {
Some(empty) => Ok(BlockData::new(BlockAddr::default(), empty)),
let addr = ptr.addr();
match T::empty(addr.level()) {
Some(empty) => Ok(BlockData::new(addr, empty)),
None => {
#[cfg(feature = "log")]
log::error!("READ_BLOCK_OR_EMPTY: INVALID BLOCK LEVEL FOR TYPE");
@@ -336,15 +337,44 @@ impl<'a, D: Disk> Transaction<'a, D> {
unsafe fn read_record<T: BlockTrait + DerefMut<Target = [u8]>>(
&mut self,
ptr: BlockPtr<T>,
mut ptr: BlockPtr<T>,
level: BlockLevel,
) -> Result<BlockData<T>> {
let record = unsafe { self.read_block_or_empty(ptr)? };
// Set null pointers to correct size (reduces number of copies below)
if ptr.is_null() {
ptr = BlockPtr::<T>::null(BlockMeta::new(level));
}
// Read record from disk, or construct empty one for null pointers
let mut record = unsafe { self.read_block_or_empty(ptr)? };
if record.addr().level() >= level {
// Return record if it is larger than or equal to requested level
return Ok(record);
}
// Attempt to decompress if address metadata indicates compression
if let Some(decomp_level) = record.addr().decomp_level() {
// First 2 bytes store compressed data length
// This means only compressed record sizes up to 64 KiB are supported
let mut decomp = match T::empty(decomp_level) {
Some(empty) => empty,
None => {
#[cfg(feature = "log")]
log::error!("READ_RECORD: INVALID DECOMPRESSED BLOCK LEVEL FOR TYPE");
return Err(Error::new(ENOENT));
}
};
let comp_len = record.data()[0] as usize | ((record.data()[1] as usize) << 8);
if let Err(err) =
lz4_flex::decompress_into(&record.data()[2..(comp_len + 2)], &mut decomp)
{
#[cfg(feature = "log")]
log::error!("READ_RECORD: FAILED TO DECOMPRESS: {:?}", err);
return Err(Error::new(EIO));
}
record = BlockData::new(BlockAddr::null(BlockMeta::new(decomp_level)), decomp);
}
// If a larger level was requested,
// create a fake record with the requested level
// and fill it with the data in the original record.
@@ -359,7 +389,8 @@ impl<'a, D: Disk> Transaction<'a, D> {
};
let len = min(raw.len(), old_raw.len());
raw[..len].copy_from_slice(&old_raw[..len]);
Ok(BlockData::new(BlockAddr::null(level), raw))
Ok(BlockData::new(BlockAddr::null(BlockMeta::new(level)), raw))
}
/// Write block data to a new address, returning new address
@@ -368,8 +399,8 @@ impl<'a, D: Disk> Transaction<'a, D> {
mut block: BlockData<T>,
) -> Result<BlockPtr<T>> {
// Swap block to new address
let level = block.addr().level();
let old_addr = block.swap_addr(unsafe { self.allocate(level)? });
let meta = block.addr().meta();
let old_addr = block.swap_addr(unsafe { self.allocate(meta)? });
// Deallocate old address (will only take effect after sync_allocator, which helps to
// prevent re-use before a new header is written
if !old_addr.is_null() {
@@ -773,7 +804,7 @@ impl<'a, D: Disk> Transaction<'a, D> {
unsafe {
let parent = self.read_tree(parent_ptr)?;
let node_block_data = BlockData::new(
self.allocate(BlockLevel::default())?,
self.allocate(BlockMeta::default())?,
Node::new(
mode,
parent.data().uid(),
@@ -1594,7 +1625,8 @@ impl<'a, D: Disk> Transaction<'a, D> {
let mut node_changed = false;
let record_level = node.data().record_level();
let node_records = node.data().size().div_ceil(record_level.bytes());
let node_size = node.data().size();
let node_records = node_size.div_ceil(record_level.bytes());
let mut i = 0;
while i < buf.len() {
@@ -1605,30 +1637,64 @@ impl<'a, D: Disk> Transaction<'a, D> {
let mut record_ptr = if node_records > (*offset / record_level.bytes()) {
self.node_record_ptr(node, *offset / record_level.bytes())?
} else {
BlockPtr::null(level)
BlockPtr::null(BlockMeta::new(level))
};
let mut record = unsafe { self.read_record(record_ptr, level)? };
// If record has changed
if buf[i..i + len] != record.data()[j..j + len] {
unsafe {
// CoW record using its current level
let mut old_addr = record.swap_addr(self.allocate(record.addr().level())?);
// Update record in memory
record.data_mut()[j..j + len].copy_from_slice(&buf[i..i + len]);
// If the record was resized we need to dealloc the original ptr
if old_addr.is_null() {
old_addr = record_ptr.addr();
}
record.data_mut()[j..j + len].copy_from_slice(&buf[i..i + len]);
record_ptr = self.write_block(record)?;
if !old_addr.is_null() {
self.deallocate(old_addr);
// Handle record compression, if record is larger than one block
let decomp_level = record.addr().level();
if decomp_level.0 > 0 {
// Maximum compressed record size is 64 KiB
let mut comp = vec![0; 64 * 1024];
// First two bytes store compressed data length
match lz4_flex::compress_into(record.data(), &mut comp[2..]) {
Ok(comp_len) => {
comp[0] = comp_len as u8;
comp[1] = (comp_len >> 8) as u8;
let comp_level = BlockLevel::for_bytes((comp_len as u64) + 2);
comp.truncate(comp_level.bytes() as usize);
// Replace record with compressed record, if it saves space
if comp_level < decomp_level {
record = BlockData::new(
BlockAddr::null(BlockMeta::new_compressed(
comp_level,
decomp_level,
)),
RecordRaw(comp.into_boxed_slice()),
);
}
}
Err(_err) => {
// Failures to compress can be ignored, with the original record data used
}
}
}
// CoW record using its current level
let mut old_addr =
unsafe { record.swap_addr(self.allocate(record.addr().meta())?) };
// If the record was resized we need to dealloc the original ptr
if old_addr.is_null() {
old_addr = record_ptr.addr();
}
// Write record to disk
record_ptr = unsafe { self.write_block(record)? };
// Update record pointer
self.sync_node_record_ptr(node, *offset / record_level.bytes(), record_ptr)?;
node_changed = true;
// Deallocate old record
if !old_addr.is_null() {
unsafe { self.deallocate(old_addr) };
}
}
i += len;