Skip to main content

blob_pager_and_verifier/
profile.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
5use crate::Cache;
6use fidl_fuchsia_storage_block as fblock;
7use fuchsia_async as fasync;
8use fuchsia_hash::Hash;
9use futures::future::{BoxFuture, join_all};
10use std::collections::{HashMap, HashSet, hash_map};
11use std::sync::Arc;
12use storage_profile::{BlobMessage, FileMessage, Message};
13
14// Matches the 4 KiB block alignment used by Fxfs on-disk profile files and `mapping::BLOCK_SIZE`
15// in the block driver (`BlobMessage` and `FileMessage` entries are packed into 4 KiB blocks
16// without crossing block boundaries).
17const PROFILE_BLOCK_SIZE: usize = 4096;
18
19// Matches `REPLAY_THREADS` in Fxfs's `profile.rs`: two blocking threads allow overlapping
20// `ZX_VMO_OP_PREFETCH` page-in latency without saturating the block driver or executor pool.
21const REPLAY_THREADS: usize = 2;
22
23struct ReplayRequest {
24    vmo: Arc<zx::Vmo>,
25    offset: u64,
26}
27
28struct ReplayHandle {
29    replay_threads: BoxFuture<'static, ()>,
30    // Holds the task that opens blobs and keeps their child VMOs cached in `local_cache` during
31    // replay. Dropping `ReplayHandle` drops this `fasync::Task`, which aborts the task and drops
32    // `local_cache` (allowing `Cache` to evict unused blobs) and `sender`.
33    _cache_task: fasync::Task<()>,
34}
35
36impl ReplayHandle {
37    fn start(cache: &Arc<Cache>, replay_vmo: zx::Vmo) -> Result<Self, zx::Status> {
38        let messages =
39            BlobMessage::decode_from_vmo(replay_vmo, PROFILE_BLOCK_SIZE).map_err(|error| {
40                log::warn!(error:?; "Failed to decode replay profile VMO");
41                if error == zx::Status::OUT_OF_RANGE {
42                    zx::Status::OUT_OF_RANGE
43                } else {
44                    zx::Status::IO_DATA_INTEGRITY
45                }
46            })?;
47
48        let (sender, receiver) = async_channel::unbounded::<ReplayRequest>();
49
50        // Create async_channel. An async task reads the profile, opens the blobs, and populates
51        // the channel, then `REPLAY_THREADS` blocking threads consume it and prefetch pages.
52        let mut replay_threads = Vec::with_capacity(REPLAY_THREADS);
53        let page_size = zx::system_get_page_size() as u64;
54        for _ in 0..REPLAY_THREADS {
55            let receiver = receiver.clone();
56            replay_threads.push(fasync::unblock(move || {
57                while let Ok(request) = receiver.recv_blocking() {
58                    if let Err(error) =
59                        request.vmo.op_range(zx::VmoOp::PREFETCH, request.offset, page_size)
60                    {
61                        log::warn!(
62                            error:?,
63                            offset = request.offset;
64                            "Failed to prefetch blob page during profile replay"
65                        );
66                    }
67                    // `sender.close()` below closes the channel while keeping `sender` alive
68                    // (`sender_count() == 1`) so replay threads finish draining queued requests.
69                    // If `ReplayHandle` is dropped early on abort, `_cache_task` is dropped along
70                    // with `sender` (`sender_count() == 0`), causing replay threads to exit here.
71                    if receiver.sender_count() == 0 {
72                        return;
73                    }
74                }
75            }));
76        }
77        let replay_threads = Box::pin(async move {
78            join_all(replay_threads).await;
79        });
80
81        let cache_clone = cache.clone();
82        let cache_task = cache
83            .scope
84            .spawn(async move {
85                // Hold the blobs in cache until replay is stopped. Optional as None indicates
86                // that the blob could not be opened, and we want to cache that failure.
87                let mut local_cache: HashMap<Hash, Option<Arc<zx::Vmo>>> = HashMap::new();
88                for msg in messages {
89                    if msg.is_open_marker() {
90                        continue;
91                    }
92                    let vmo = match local_cache.entry(msg.id) {
93                        hash_map::Entry::Occupied(entry) => match entry.get() {
94                            Some(vmo) => vmo.clone(),
95                            // Found a cached error.
96                            None => continue,
97                        },
98                        hash_map::Entry::Vacant(entry) => {
99                            match cache_clone.open_vmo(&msg.id.into()).await {
100                                Ok(vmo) => {
101                                    let vmo = Arc::new(vmo);
102                                    entry.insert(Some(vmo.clone()));
103                                    vmo
104                                }
105                                Err(error) => {
106                                    log::debug!(
107                                        error:?,
108                                        hash:? = msg.id;
109                                        "Failed to open blob from profile replay"
110                                    );
111                                    // Cache the error.
112                                    entry.insert(None);
113                                    continue;
114                                }
115                            }
116                        }
117                    };
118                    if let Err(error) = sender.send(ReplayRequest { vmo, offset: msg.offset }).await
119                    {
120                        log::warn!(error:?; "Failed to send request to replay thread");
121                        break;
122                    }
123                }
124                drop(cache_clone);
125                sender.close();
126                // Keep `local_cache` alive so replayed blobs stay cached until replay is stopped.
127                let () = std::future::pending().await;
128            })
129            .into();
130
131        Ok(Self { replay_threads, _cache_task: cache_task })
132    }
133}
134
135/// An active profile recording and replay session driven by [`crate::BlobPagerAndVerifier`].
136///
137/// If dropped without calling [`ProfileRecording::stop`], any in-flight replay and driver
138/// recording are automatically aborted without writing a profile to the filesystem.
139pub struct ProfileRecording {
140    name: String,
141    cache: Arc<Cache>,
142    recording_proxy: Option<fblock::RecordingProxy>,
143    replay_handle: Option<ReplayHandle>,
144}
145
146impl std::fmt::Debug for ProfileRecording {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("ProfileRecording")
149            .field("name", &self.name)
150            .field("has_replay", &self.replay_handle.is_some())
151            .finish_non_exhaustive()
152    }
153}
154
155impl ProfileRecording {
156    pub(crate) async fn start(
157        cache: Arc<Cache>,
158        mapper_session: &fblock::MapperSessionProxy,
159        name: String,
160    ) -> Result<Self, zx::Status> {
161        cache.start_recording()?;
162        // Construct `Self` immediately so `Drop` calls `self.cache.abort_recording()` if any
163        // subsequent step fails or if the `start` future is cancelled at an `.await` point.
164        let mut this = Self { name, cache, recording_proxy: None, replay_handle: None };
165
166        let (recording_proxy, recording_server) =
167            fidl::endpoints::create_proxy::<fblock::RecordingMarker>();
168        match mapper_session.record_profile(recording_server).await {
169            Ok(Ok(())) => {}
170            Ok(Err(raw_status)) => return Err(zx::Status::err_from_raw(raw_status)),
171            Err(error) => {
172                log::warn!(error:?; "FIDL error calling MapperSession.RecordProfile");
173                return Err(zx::Status::INTERNAL);
174            }
175        }
176        this.recording_proxy = Some(recording_proxy);
177
178        let replay_vmo = match this.cache.mapping_session.get_profile(&this.name).await {
179            Ok(Ok(vmo)) => Some(vmo),
180            Ok(Err(raw_status)) => {
181                let status = zx::Status::err_from_raw(raw_status);
182                if status == zx::Status::NOT_FOUND {
183                    None
184                } else {
185                    log::warn!(
186                        status:?,
187                        name = this.name.as_str();
188                        "MappingSession.GetProfile failed"
189                    );
190                    return Err(status);
191                }
192            }
193            Err(error) => {
194                log::warn!(
195                    error:?,
196                    name = this.name.as_str();
197                    "FIDL error calling MappingSession.GetProfile"
198                );
199                return Err(zx::Status::INTERNAL);
200            }
201        };
202
203        if let Some(vmo) = replay_vmo {
204            this.replay_handle = Some(ReplayHandle::start(&this.cache, vmo)?);
205        }
206
207        Ok(this)
208    }
209
210    /// Stops any active replay, finalizes the driver recording, filters by blobs opened during the
211    /// recording window, and writes the encoded profile to the filesystem via `MappingSession`.
212    pub async fn stop(mut self) -> Result<(), zx::Status> {
213        if let Some(replay_handle) = self.replay_handle.take() {
214            replay_handle.replay_threads.await;
215        }
216
217        let recording_proxy = self.recording_proxy.take().ok_or(zx::Status::BAD_STATE)?;
218        let stop_res = recording_proxy.stop().await;
219        let active_recording = self.cache.take_recording().ok_or(zx::Status::BAD_STATE)?;
220
221        let driver_vmo = match stop_res {
222            Ok(Ok(vmo)) => vmo,
223            Ok(Err(raw_status)) => return Err(zx::Status::err_from_raw(raw_status)),
224            Err(error) => {
225                log::warn!(error:?; "FIDL error calling Recording.Stop");
226                return Err(zx::Status::INTERNAL);
227            }
228        };
229
230        let file_messages =
231            FileMessage::decode_from_vmo(driver_vmo, PROFILE_BLOCK_SIZE).map_err(|error| {
232                if error == zx::Status::OUT_OF_RANGE {
233                    log::warn!("Driver profile VMO exceeds maximum allowed size");
234                    zx::Status::OUT_OF_RANGE
235                } else {
236                    log::warn!(error:?; "Failed to decode driver profile VMO");
237                    zx::Status::IO_DATA_INTEGRITY
238                }
239            })?;
240
241        let mut seen = HashSet::new();
242        let blob_messages = file_messages
243            .filter_map(|msg| {
244                let &hash = active_recording.hash_by_key.get(&msg.id)?;
245                active_recording
246                    .opened_hashes
247                    .contains(&hash)
248                    .then_some(BlobMessage { id: hash, offset: msg.offset })
249            })
250            .filter(|&blob_msg| seen.insert(blob_msg));
251
252        let recorded_vmo = BlobMessage::encode_to_vmo(blob_messages, PROFILE_BLOCK_SIZE)?;
253
254        match self.cache.mapping_session.write_profile(&self.name, recorded_vmo).await {
255            Ok(Ok(())) => Ok(()),
256            Ok(Err(raw_status)) => {
257                let status = zx::Status::err_from_raw(raw_status);
258                log::warn!(
259                    status:?,
260                    name = self.name.as_str();
261                    "MappingSession.WriteProfile failed"
262                );
263                Err(status)
264            }
265            Err(error) => {
266                log::warn!(
267                    error:?,
268                    name = self.name.as_str();
269                    "FIDL error calling MappingSession.WriteProfile"
270                );
271                Err(zx::Status::INTERNAL)
272            }
273        }
274    }
275}
276
277impl Drop for ProfileRecording {
278    fn drop(&mut self) {
279        self.cache.abort_recording();
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::BlobPagerAndVerifier;
287    use fidl_fuchsia_storage_mapping as fmapping;
288    use fuchsia_sync::Mutex;
289    use futures::channel::oneshot;
290    use futures::{StreamExt, TryStreamExt};
291    use mapping::{DeliveryCommand, RawDeliveryCommand};
292    use vmo_fifo::SyncSender;
293
294    #[fuchsia::test]
295    async fn test_profile_recorder_replay_filtering_and_eviction() {
296        let (mapping_proxy, mut mapping_stream) =
297            fidl::endpoints::create_proxy_and_stream::<fmapping::MappingProviderMarker>();
298        let (mapper_proxy, mut mapper_stream) =
299            fidl::endpoints::create_proxy_and_stream::<fblock::MapperMarker>();
300
301        let blob_data = vec![0x42u8; 8192];
302        let (root1, _) = fuchsia_merkle::MerkleRootBuilder::new(Vec::new()).complete(&blob_data);
303        let hash1: [u8; 32] = root1.into();
304        let blob_data2 = vec![0x24u8; 8192];
305        let (root2, _) = fuchsia_merkle::MerkleRootBuilder::new(Vec::new()).complete(&blob_data2);
306        let hash2: [u8; 32] = root2.into();
307        let blob_data3 = vec![0x99u8; 8192];
308        let (root3, _) = fuchsia_merkle::MerkleRootBuilder::new(Vec::new()).complete(&blob_data3);
309        let hash3: [u8; 32] = root3.into();
310        let missing_hash: [u8; 32] = [0xde; 32];
311
312        // Replay profile contains `missing_hash` (twice, to verify negative lookup caching),
313        // `hash1` (which a client will also open), and `hash2` (replay-only, never opened by a
314        // client).
315        let replay_entries = vec![
316            BlobMessage { id: Hash::from(missing_hash), offset: 0 },
317            BlobMessage { id: Hash::from(missing_hash), offset: 4096 },
318            BlobMessage { id: Hash::from(hash1), offset: 0 },
319            BlobMessage { id: Hash::from(hash2), offset: 0 },
320        ];
321        let next_replay_vmo = Arc::new(Mutex::new(Some(
322            BlobMessage::encode_to_vmo(&replay_entries, PROFILE_BLOCK_SIZE)
323                .expect("encode_to_vmo failed"),
324        )));
325        let next_replay_vmo_for_server = next_replay_vmo.clone();
326
327        let (written_tx, written_rx) = oneshot::channel();
328        let (close_tx, mut close_rx) = futures::channel::mpsc::unbounded();
329        let opened_keys = Arc::new(Mutex::new(HashMap::<[u8; 32], Vec<u64>>::new()));
330        let opened_keys_clone = opened_keys.clone();
331
332        let mapping_task = fasync::Task::spawn(async move {
333            let mut written_tx = Some(written_tx);
334            if let Some(fmapping::MappingProviderRequest::OpenSession { session, responder }) =
335                mapping_stream.try_next().await.expect("try_next failed")
336            {
337                let mapping_vmo = zx::Vmo::create(zx::system_get_page_size().into())
338                    .expect("zx::Vmo::create failed");
339                responder.send(Ok(mapping_vmo)).expect("send failed");
340
341                let mut session_stream = session.into_stream();
342                while let Some(request) = session_stream.try_next().await.expect("try_next failed")
343                {
344                    match request {
345                        fmapping::MappingSessionRequest::GetProfile { name, responder } => {
346                            assert_eq!(name, ".boot");
347                            match next_replay_vmo_for_server.lock().take() {
348                                Some(vmo) => responder.send(Ok(vmo)).expect("send failed"),
349                                None => responder
350                                    .send(Err(zx::Status::NOT_FOUND.into_raw()))
351                                    .expect("send failed"),
352                            }
353                        }
354                        fmapping::MappingSessionRequest::WriteProfile {
355                            name,
356                            profile_vmo,
357                            responder,
358                        } => {
359                            assert_eq!(name, ".boot");
360                            if let Some(tx) = written_tx.take() {
361                                let _ = tx.send(profile_vmo);
362                            }
363                            responder.send(Ok(())).expect("send failed");
364                        }
365                        fmapping::MappingSessionRequest::Open { key, identifier, responder } => {
366                            let arr: [u8; 32] =
367                                identifier.as_slice().try_into().expect("invalid hash slice");
368                            opened_keys_clone.lock().entry(arr).or_default().push(key);
369                            if arr == missing_hash {
370                                responder
371                                    .send(Err(zx::Status::NOT_FOUND.into_raw()))
372                                    .expect("send failed");
373                            } else {
374                                responder.send(Ok(8192)).expect("send failed");
375                            }
376                        }
377                        fmapping::MappingSessionRequest::Close { key, responder } => {
378                            responder.send(Ok(())).expect("send failed");
379                            let _ = close_tx.unbounded_send(key);
380                        }
381                        _ => {}
382                    }
383                }
384            }
385        });
386
387        let (tx_delivery, rx_delivery) = oneshot::channel();
388        let (recording_dropped_tx, recording_dropped_rx) = oneshot::channel();
389        let opened_keys_for_mapper = opened_keys.clone();
390        let mapper_task = fasync::Task::spawn(async move {
391            let mut recording_dropped_tx = Some(recording_dropped_tx);
392            if let Some(fblock::MapperRequest::OpenSession {
393                session,
394                port,
395                delivery_queue,
396                responder,
397                ..
398            }) = mapper_stream.try_next().await.expect("try_next failed")
399            {
400                let port = port.expect("expected port");
401                let delivery_vmo = delivery_queue.expect("expected delivery_queue");
402                tx_delivery
403                    .send(
404                        delivery_vmo
405                            .duplicate_handle(zx::Rights::SAME_RIGHTS)
406                            .expect("duplicate_handle failed"),
407                    )
408                    .expect("send failed");
409                responder.send(Ok(())).expect("send failed");
410
411                // Spawn a mock pager thread on `port` that supplies pages when `ZX_VMO_OP_PREFETCH`
412                // triggers a page request during replay.
413                let opened_keys_for_pager = opened_keys_for_mapper.clone();
414                std::thread::spawn(move || {
415                    let mut sender = SyncSender::<RawDeliveryCommand>::new(
416                        delivery_vmo,
417                        zx::system_get_page_size() as usize,
418                        mapping::PENDING_DELIVERY_COMMANDS_CAPACITY,
419                    )
420                    .expect("SyncSender::new failed");
421                    while let Ok(packet) = port.wait(zx::MonotonicInstant::INFINITE) {
422                        if let zx::PacketContents::Pager(pager_packet) = packet.contents() {
423                            if pager_packet.command()
424                                == zx::sys::zx_page_request_command_t::ZX_PAGER_VMO_READ
425                            {
426                                let key = packet.key();
427                                let range = pager_packet.range();
428                                let reg_payload =
429                                    sender.reserve_payload(0).expect("reserve_payload failed");
430                                let reg_cmd: RawDeliveryCommand = DeliveryCommand::RegisterBlob {
431                                    key,
432                                    offset: reg_payload.offset(),
433                                    length: 0,
434                                    read_alignment: 8192,
435                                }
436                                .into();
437                                reg_payload.commit(reg_cmd).expect("commit failed");
438
439                                let data = if opened_keys_for_pager
440                                    .lock()
441                                    .get(&hash1)
442                                    .is_some_and(|ks| ks.contains(&key))
443                                {
444                                    &blob_data
445                                } else {
446                                    &blob_data2
447                                };
448                                let mut payload = sender
449                                    .reserve_payload(data.len())
450                                    .expect("reserve_payload failed");
451                                payload.data().copy_from_slice(data);
452                                let cmd: RawDeliveryCommand = DeliveryCommand::Data {
453                                    key,
454                                    offset: payload.offset(),
455                                    length: data.len() as u32,
456                                    target_offset: range.start,
457                                }
458                                .into();
459                                payload.commit(cmd).expect("commit failed");
460                            }
461                        }
462                    }
463                });
464
465                let mut session_stream = session.into_stream();
466                while let Some(request) = session_stream.try_next().await.expect("try_next failed")
467                {
468                    if let fblock::MapperSessionRequest::RecordProfile { recording, responder } =
469                        request
470                    {
471                        responder.send(Ok(())).expect("send failed");
472                        let mut rec_stream = recording.into_stream();
473                        match rec_stream.try_next().await.expect("try_next failed") {
474                            Some(fblock::RecordingRequest::Stop { responder }) => {
475                                let keys = opened_keys_for_mapper.lock().clone();
476                                let k1 = keys.get(&hash1).expect("hash1 keys missing")[0];
477                                let k2 = keys.get(&hash2).expect("hash2 keys missing")[0];
478                                let k3_first = keys.get(&hash3).expect("hash3 keys missing")[0];
479                                let k3_second = keys.get(&hash3).expect("hash3 keys missing")[1];
480                                let driver_entries = vec![
481                                    FileMessage { id: k1, offset: 0 },
482                                    FileMessage { id: k2, offset: 0 },
483                                    FileMessage { id: k3_first, offset: 0 },
484                                    // Duplicate `(hash3, 0)` under the reopened key `k3_second`
485                                    FileMessage { id: k3_second, offset: 0 },
486                                    FileMessage { id: k3_second, offset: 4096 },
487                                ];
488                                let vmo =
489                                    FileMessage::encode_to_vmo(&driver_entries, PROFILE_BLOCK_SIZE)
490                                        .expect("encode_to_vmo failed");
491                                responder.send(Ok(vmo)).expect("send failed");
492                            }
493                            None => {
494                                if let Some(tx) = recording_dropped_tx.take() {
495                                    let _ = tx.send(());
496                                }
497                            }
498                            _ => {}
499                        }
500                    }
501                }
502            }
503        });
504
505        let pager_and_verifier = Arc::new(
506            BlobPagerAndVerifier::new(&mapping_proxy, &mapper_proxy)
507                .await
508                .expect("BlobPagerAndVerifier::new failed"),
509        );
510        let _delivery_vmo = rx_delivery.await.expect("rx_delivery failed");
511
512        let recording =
513            pager_and_verifier.start_profile(".boot").await.expect("start_profile failed");
514
515        // Concurrent `start_profile` while recording is active must fail with `BAD_STATE`.
516        let err = pager_and_verifier
517            .start_profile(".boot")
518            .await
519            .expect_err("Concurrent start_profile succeeded");
520        assert_eq!(err, zx::Status::BAD_STATE);
521
522        // Wait for the failed open of `missing_hash` to clean up its key reservation via `Close`.
523        let missing_closed_key = close_rx.next().await.expect("expected close for missing_hash");
524        assert_eq!(
525            missing_closed_key,
526            opened_keys.lock().get(&missing_hash).expect("missing_hash key missing")[0]
527        );
528
529        // Client opens `hash1` and `hash3` (leaving `hash2` unopened by any client).
530        let vmo1 = pager_and_verifier.create_vmo(&hash1).await.expect("create_vmo hash1 failed");
531        let vmo3 = pager_and_verifier.create_vmo(&hash3).await.expect("create_vmo hash3 failed");
532
533        // Evict `hash3` and re-open it before `stop()` to verify `hash_by_key` retains evicted
534        // keys and deduplicates `(hash3, 0)` across keys.
535        drop(vmo3);
536        let _ = close_rx.next().await.expect("expected close for first vmo3");
537        let vmo3_reopened =
538            pager_and_verifier.create_vmo(&hash3).await.expect("re-open hash3 failed");
539        drop(vmo3_reopened);
540        let _ = close_rx.next().await.expect("expected close for second vmo3");
541
542        // Stop recording: this waits for replay to finish, unpins `hash2` (causing its eviction),
543        // filters out `hash2`, retains `hash1` and `hash3`, deduplicates `(hash3, 0)`, and writes
544        // the profile VMO via `MappingSession.WriteProfile`.
545        recording.stop().await.expect("recording.stop failed");
546        let recorded_vmo = written_rx.await.expect("written_rx failed");
547
548        // Verify negative lookup caching: `missing_hash` appeared twice in `replay_entries`, so
549        // `MappingSession.Open` must have been called only once for it.
550        assert_eq!(
551            opened_keys.lock().get(&missing_hash).expect("missing_hash key missing").len(),
552            1
553        );
554
555        // `hash2` was only opened by replay and must be evicted upon `stop()`.
556        let closed_key = close_rx.next().await.expect("expected close for unpinned replay blob");
557        assert_eq!(closed_key, opened_keys.lock().get(&hash2).expect("hash2 key missing")[0]);
558
559        let decoded: Vec<_> = BlobMessage::decode_from_vmo(recorded_vmo, PROFILE_BLOCK_SIZE)
560            .expect("decode_from_vmo failed")
561            .collect();
562        assert_eq!(
563            decoded,
564            vec![
565                BlobMessage { id: Hash::from(hash1), offset: 0 },
566                BlobMessage { id: Hash::from(hash3), offset: 0 },
567                BlobMessage { id: Hash::from(hash3), offset: 4096 },
568            ]
569        );
570
571        // Start a second recording and drop `ProfileRecording` without calling `stop()` to verify
572        // that the driver recording is aborted and replay-pinned blobs are unpinned.
573        *next_replay_vmo.lock() = Some(
574            BlobMessage::encode_to_vmo(
575                &[BlobMessage { id: Hash::from(hash2), offset: 0 }],
576                PROFILE_BLOCK_SIZE,
577            )
578            .expect("encode_to_vmo failed"),
579        );
580        let recording2 =
581            pager_and_verifier.start_profile(".boot").await.expect("second start_profile failed");
582        drop(recording2);
583        recording_dropped_rx.await.expect("expected driver Recording channel to close on abort");
584
585        drop(vmo1);
586        let _ = close_rx.next().await.expect("expected close for vmo1 or aborted replay");
587        drop(pager_and_verifier);
588        mapping_task.await;
589        mapper_task.await;
590    }
591}