Skip to main content

storage_profile/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Shared profile message types and block-aligned VMO encoding/decoding used across Fxfs,
6//! `blob_pager_and_verifier`, and `mapping` / `block_server`.
7
8use arrayref::{array_refs, mut_array_refs};
9use fuchsia_hash::Hash;
10use std::borrow::Borrow;
11use std::marker::PhantomData;
12use std::mem::size_of;
13use storage_ptr_slice::{MutPtrByteSlice, PtrByteSlice};
14
15/// Sentinel offset used by Fxfs to record that a file or blob was opened during a recording window.
16pub const FILE_OPEN_MARKER: u64 = u64::MAX;
17
18/// Upper bound on profile VMO size to prevent unbounded memory growth.
19pub const MAX_PROFILE_VMO_SIZE: u64 = 64 * 1024 * 1024;
20
21// Streams [`Message`] entries into a resizable `zx::Vmo` in `block_size`-aligned chunks without
22// crossing block boundaries.
23struct VmoWriter {
24    vmo: zx::Vmo,
25    vmo_offset: u64,
26    block_size: usize,
27    buffer: Vec<u8>,
28}
29
30impl VmoWriter {
31    fn new(block_size: usize) -> Result<Self, zx::Status> {
32        let vmo = zx::Vmo::create_with_opts(zx::VmoOptions::RESIZABLE, 0)?;
33        Ok(Self { vmo, vmo_offset: 0, block_size, buffer: Vec::with_capacity(block_size) })
34    }
35
36    fn write<M: Message>(&mut self, msg: &M) -> Result<(), zx::Status> {
37        let msg_size = size_of::<M>();
38        if msg_size == 0 || self.block_size < msg_size {
39            return Ok(());
40        }
41        // Profiles are best-effort prefetch hints, so once the VMO reaches `MAX_PROFILE_VMO_SIZE`,
42        // drop additional entries to cap memory usage rather than failing and discarding the entire
43        // profile.
44        if self.vmo_offset + self.block_size as u64 > MAX_PROFILE_VMO_SIZE {
45            return Ok(());
46        }
47        let start = self.buffer.len();
48        self.buffer.resize(start + msg_size, 0);
49        msg.encode_to(MutPtrByteSlice::from(&mut self.buffer[start..]));
50        // Flush if another message cannot fit in this block.
51        if self.buffer.len() + msg_size > self.block_size {
52            self.flush()?;
53        }
54        Ok(())
55    }
56
57    fn flush(&mut self) -> Result<(), zx::Status> {
58        let new_size = self.vmo_offset + self.block_size as u64;
59        let res = self.vmo.set_size(new_size).and_then(|()| {
60            self.vmo.write(&self.buffer, self.vmo_offset)?;
61            self.vmo_offset = new_size;
62            Ok(())
63        });
64        self.buffer.clear();
65        res
66    }
67
68    fn finish(mut self) -> Result<zx::Vmo, zx::Status> {
69        if !self.buffer.is_empty() {
70            self.flush()?;
71        }
72        Ok(self.vmo)
73    }
74}
75
76/// Reads and decodes [`Message`] entries from a `zx::Vmo` one block at a time.
77#[derive(Debug)]
78pub struct DecodedMessageIter<M> {
79    vmo: zx::Vmo,
80    vmo_size: u64,
81    vmo_offset: u64,
82    block_size: usize,
83    block_buf: Vec<u8>,
84    block_offset: usize,
85    _marker: PhantomData<M>,
86}
87
88impl<M: Message> DecodedMessageIter<M> {
89    fn new(vmo: zx::Vmo, block_size: usize) -> Result<Self, zx::Status> {
90        let vmo_size = vmo.get_size()?;
91        if vmo_size > MAX_PROFILE_VMO_SIZE {
92            return Err(zx::Status::OUT_OF_RANGE);
93        }
94        Ok(Self {
95            vmo,
96            vmo_size,
97            vmo_offset: 0,
98            block_size,
99            block_buf: Vec::with_capacity(block_size),
100            block_offset: 0,
101            _marker: PhantomData,
102        })
103    }
104}
105
106impl<M: Message> Iterator for DecodedMessageIter<M> {
107    type Item = M;
108
109    fn next(&mut self) -> Option<Self::Item> {
110        let msg_size = size_of::<M>();
111        if msg_size == 0 || self.block_size < msg_size {
112            return None;
113        }
114        if self.block_offset + msg_size > self.block_buf.len() {
115            if self.vmo_offset + self.block_size as u64 > self.vmo_size {
116                return None;
117            }
118            self.block_buf.resize(self.block_size, 0);
119            if self.vmo.read(&mut self.block_buf, self.vmo_offset).is_err() {
120                return None;
121            }
122            self.vmo_offset += self.block_size as u64;
123            self.block_offset = 0;
124        }
125        let entry_bytes = &self.block_buf[self.block_offset..self.block_offset + msg_size];
126        self.block_offset += msg_size;
127        let msg = M::decode_from(PtrByteSlice::from(entry_bytes));
128        // Trailing zero-padding in the final block marks the end of the recorded entries.
129        if msg.is_zeroes() {
130            self.vmo_offset = self.vmo_size;
131            return None;
132        }
133        Some(msg)
134    }
135}
136
137/// A fixed-size profile entry recording an access to `(id, offset)`.
138pub trait Message:
139    Eq + PartialEq + Sized + Send + Sync + std::hash::Hash + Copy + Clone + 'static
140{
141    type IdType: std::fmt::Display + Ord + Send + Sized + Copy + Clone;
142
143    fn id(&self) -> Self::IdType;
144    fn offset(&self) -> u64;
145    fn encode_to(&self, dest: MutPtrByteSlice<'_>);
146    fn decode_from(src: PtrByteSlice<'_>) -> Self;
147    fn is_zeroes(&self) -> bool;
148
149    fn is_open_marker(&self) -> bool {
150        self.offset() == FILE_OPEN_MARKER
151    }
152
153    /// Encodes `messages` into a new `zx::Vmo` packed into `block_size`-aligned blocks (typically
154    /// 4 KiB) without crossing `block_size` boundaries. Any trailing bytes in a block that cannot
155    /// fit a full message are zero-padded.
156    fn encode_to_vmo(
157        messages: impl IntoIterator<Item = impl Borrow<Self>>,
158        block_size: usize,
159    ) -> Result<zx::Vmo, zx::Status> {
160        let mut writer = VmoWriter::new(block_size)?;
161        for msg in messages {
162            writer.write(msg.borrow())?;
163        }
164        writer.finish()
165    }
166
167    /// Returns an iterator that lazily decodes messages from `vmo` one `block_size`-aligned block
168    /// at a time. An all-zero message (`Self::is_zeroes()`) marks the end of the recorded stream.
169    fn decode_from_vmo(
170        vmo: zx::Vmo,
171        block_size: usize,
172    ) -> Result<DecodedMessageIter<Self>, zx::Status> {
173        DecodedMessageIter::new(vmo, block_size)
174    }
175}
176
177/// A 40-byte profile entry identifying a blob by its 32-byte Merkle root [`Hash`] and a logical
178/// byte `offset`.
179#[derive(Copy, Clone, Debug, Eq, std::hash::Hash, PartialEq)]
180pub struct BlobMessage {
181    pub id: Hash,
182    /// Readahead-aligned logical byte offset within the blob. Only the start offset (without
183    /// length) is stored because replay prefetches a single page at `offset` and relies on the
184    /// pager's readahead to fault in the full window.
185    pub offset: u64,
186}
187
188impl Message for BlobMessage {
189    type IdType = Hash;
190
191    fn id(&self) -> Self::IdType {
192        self.id
193    }
194
195    fn offset(&self) -> u64 {
196        self.offset
197    }
198
199    fn encode_to(&self, mut dest: MutPtrByteSlice<'_>) {
200        let mut buf = [0u8; size_of::<Self>()];
201        let (first, second) = mut_array_refs![&mut buf, size_of::<Hash>(), size_of::<u64>()];
202        *first = self.id.into();
203        *second = self.offset.to_le_bytes();
204        dest.copy_from_slice(&buf);
205    }
206
207    fn decode_from(src: PtrByteSlice<'_>) -> Self {
208        let mut buf = [0u8; size_of::<Self>()];
209        src.copy_to_slice(&mut buf);
210        let (first, second) = array_refs!(&buf, size_of::<Hash>(), size_of::<u64>());
211        Self { id: Hash::from_array(*first), offset: u64::from_le_bytes(*second) }
212    }
213
214    fn is_zeroes(&self) -> bool {
215        self.id == Hash::from_array([0u8; size_of::<Hash>()]) && self.offset == 0
216    }
217}
218
219/// A 16-byte profile entry identifying a file by a numeric `id` (Fxfs object ID or mapper session
220/// `key`) and a logical byte `offset`.
221#[derive(Copy, Clone, Debug, Eq, std::hash::Hash, PartialEq)]
222pub struct FileMessage {
223    pub id: u64,
224    /// Readahead-aligned logical byte offset within the file. Only the start offset (without
225    /// length) is stored because replay prefetches a single page at `offset` and relies on the
226    /// pager's readahead to fault in the full window.
227    pub offset: u64,
228}
229
230impl Message for FileMessage {
231    type IdType = u64;
232
233    fn id(&self) -> Self::IdType {
234        self.id
235    }
236
237    fn offset(&self) -> u64 {
238        self.offset
239    }
240
241    fn encode_to(&self, mut dest: MutPtrByteSlice<'_>) {
242        let mut buf = [0u8; size_of::<Self>()];
243        let (first, second) = mut_array_refs![&mut buf, size_of::<u64>(), size_of::<u64>()];
244        *first = self.id.to_le_bytes();
245        *second = self.offset.to_le_bytes();
246        dest.copy_from_slice(&buf);
247    }
248
249    fn decode_from(src: PtrByteSlice<'_>) -> Self {
250        let mut buf = [0u8; size_of::<Self>()];
251        src.copy_to_slice(&mut buf);
252        let (first, second) = array_refs!(&buf, size_of::<u64>(), size_of::<u64>());
253        Self { id: u64::from_le_bytes(*first), offset: u64::from_le_bytes(*second) }
254    }
255
256    fn is_zeroes(&self) -> bool {
257        self.id == 0 && self.offset == 0
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    const BLOCK_SIZE: usize = 4096;
266
267    #[fuchsia::test]
268    fn test_encode_decode_blob() {
269        let mut buf = [0u8; size_of::<BlobMessage>()];
270        let m = BlobMessage { id: [88u8; 32].into(), offset: 77 };
271        m.encode_to(MutPtrByteSlice::from(&mut buf[..]));
272        let m2 = BlobMessage::decode_from(PtrByteSlice::from(&buf[..]));
273        assert!(!m2.is_zeroes());
274        assert_eq!(m, m2);
275    }
276
277    #[fuchsia::test]
278    fn test_encode_decode_file() {
279        let mut buf = [0u8; size_of::<FileMessage>()];
280        let m = FileMessage { id: 88, offset: 77 };
281        m.encode_to(MutPtrByteSlice::from(&mut buf[..]));
282        let m2 = FileMessage::decode_from(PtrByteSlice::from(&buf[..]));
283        assert!(!m2.is_zeroes());
284        assert_eq!(m, m2);
285    }
286
287    #[fuchsia::test]
288    fn test_file_message_vmo_round_trip() {
289        let empty_vmo = FileMessage::encode_to_vmo(&[] as &[FileMessage], BLOCK_SIZE)
290            .expect("encode_to_vmo failed");
291        assert_eq!(
292            FileMessage::decode_from_vmo(empty_vmo, BLOCK_SIZE)
293                .expect("decode_from_vmo failed")
294                .collect::<Vec<_>>(),
295            Vec::<FileMessage>::new()
296        );
297
298        // Exact block multiple (256 * 16 = 4096 bytes) with no trailing zeroes.
299        let exact_block: Vec<FileMessage> =
300            (0..256).map(|i| FileMessage { id: (i % 7) + 1, offset: i * 4096 }).collect();
301        let exact_vmo =
302            FileMessage::encode_to_vmo(&exact_block, BLOCK_SIZE).expect("encode_to_vmo failed");
303        assert_eq!(
304            FileMessage::decode_from_vmo(exact_vmo, BLOCK_SIZE)
305                .expect("decode_from_vmo failed")
306                .collect::<Vec<_>>(),
307            exact_block
308        );
309
310        // Multi-block with partial second block (260 > 256 entries per 4 KiB block).
311        let messages: Vec<FileMessage> =
312            (0..260).map(|i| FileMessage { id: (i % 7) + 1, offset: i * 4096 }).collect();
313        let vmo = FileMessage::encode_to_vmo(&messages, BLOCK_SIZE).expect("encode_to_vmo failed");
314        let decoded: Vec<_> = FileMessage::decode_from_vmo(vmo, BLOCK_SIZE)
315            .expect("decode_from_vmo failed")
316            .collect();
317        assert_eq!(decoded, messages);
318    }
319
320    #[fuchsia::test]
321    fn test_blob_message_vmo_round_trip_across_blocks() {
322        // 102 * 40 = 4080 bytes per 4 KiB block (with 16 bytes zero padding at the end of block 0).
323        // Test 105 messages so we cross the 4 KiB block boundary and verify the 16-byte padding at
324        // the end of the first block is skipped rather than misinterpreted as an end-of-profile
325        // zero entry.
326        let messages: Vec<BlobMessage> = (0..105)
327            .map(|i| BlobMessage { id: [(i as u8) + 1; 32].into(), offset: (i as u64) * 131072 })
328            .collect();
329        let vmo = BlobMessage::encode_to_vmo(&messages, BLOCK_SIZE).expect("encode_to_vmo failed");
330        assert_eq!(vmo.get_size().expect("get_size failed"), (2 * BLOCK_SIZE) as u64);
331
332        let decoded: Vec<_> = BlobMessage::decode_from_vmo(vmo, BLOCK_SIZE)
333            .expect("decode_from_vmo failed")
334            .collect();
335        assert_eq!(decoded, messages);
336    }
337}