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