Skip to main content

fxfs_platform_testing/fuchsia/
profile.rs

1// Copyright 2024 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::fuchsia::file::FxFile;
6use crate::fuchsia::fxblob::BlobDirectory;
7use crate::fuchsia::fxblob::blob::FxBlob;
8use crate::fuchsia::node::{FxNode, OpenedNode};
9use crate::fuchsia::pager::PagerBacked;
10use crate::fuchsia::volume::FxVolume;
11use anyhow::{Context as _, Error, anyhow, ensure};
12use async_trait::async_trait;
13use fuchsia_async as fasync;
14use fuchsia_hash::Hash;
15use futures::future::{self, BoxFuture, join_all};
16use futures::lock::Mutex;
17use futures::{FutureExt, select};
18use fxfs::errors::FxfsError;
19use fxfs::log::*;
20use fxfs::object_handle::{INVALID_OBJECT_ID, ObjectHandle, ReadObjectHandle, WriteObjectHandle};
21use fxfs::object_store::transaction::{LockKey, Options, lock_keys};
22use fxfs::object_store::{
23    AttributeId, AttributeKey, DataObjectHandle, HandleOptions, Mutation, ObjectDescriptor,
24    ObjectKey, ObjectStore, ObjectValue, Timestamp, VOLUME_DATA_KEY_ID, directory,
25};
26use linked_hash_map::LinkedHashMap;
27use scopeguard::ScopeGuard;
28use std::collections::btree_map::{BTreeMap, Entry};
29use std::marker::PhantomData;
30use std::mem::size_of;
31use std::pin::pin;
32use std::sync::Arc;
33use std::sync::atomic::{AtomicU64, Ordering};
34use storage_device::buffer::{BufferFuture, BufferRef};
35use storage_profile::{BlobMessage, FILE_OPEN_MARKER, FileMessage, Message};
36use storage_units::page_size;
37use vfs::execution_scope::ActiveGuard;
38
39const REPLAY_THREADS: usize = 2;
40// The number of messages to buffer before sending to record. They are chunked up to reduce the
41// number of allocations in the serving threads.
42const MESSAGE_CHUNK_SIZE: usize = 64;
43pub(crate) const IO_SIZE: usize = 1 << 17; // 128KiB. Needs to be a power of 2 and >= block size.
44
45pub static RECORDED: AtomicU64 = AtomicU64::new(0);
46
47/// A handle for recording a profile to.
48#[async_trait]
49pub trait RecordingHandle: Send + Sync {
50    /// Append data to the handle.
51    async fn append<'a>(&'a self, buf: BufferRef<'a>) -> Result<u64, Error>;
52
53    fn allocate_buffer(&self, size: usize) -> BufferFuture<'_>;
54
55    fn block_size(&self) -> usize;
56
57    /// The recording is finished being appended to the file. Commit it.
58    async fn commit(self: Box<Self>) -> Result<(), Error>;
59
60    /// When the recording fails or is stopped prematurely this will be called to clean up the
61    /// resources, delete the backing data.
62    fn abort_cleanup(self: Box<Self>);
63
64    /// Returns true if events should be filtered based on whether the file was opened during the
65    /// recording and encryption settings. For now this is true for FileRecordingHandle and false
66    /// for AssociatedRecordingHandle.
67    fn filter_events(&self) -> bool;
68}
69
70/// For placing the recording in the volume's internal profile directory.
71pub struct FileRecordingHandle {
72    name: String,
73    volume: Arc<FxVolume>,
74    handle: DataObjectHandle<FxVolume>,
75}
76
77impl FileRecordingHandle {
78    pub async fn new(name: &str, volume: Arc<FxVolume>) -> Result<Self, Error> {
79        let store = volume.store();
80        let mut transaction = store.new_transaction(lock_keys![], Options::default()).await?;
81        let handle =
82            ObjectStore::create_object(&volume, &mut transaction, HandleOptions::default(), None)
83                .await?;
84        store.add_to_graveyard(&mut transaction, handle.object_id());
85        transaction.commit().await?;
86
87        Ok(Self { name: name.to_string(), volume, handle })
88    }
89
90    async fn commit_impl(&self) -> Result<(), Error> {
91        let store = self.volume.store();
92        let fs = store.filesystem();
93        let profile_dir = self.volume.get_profile_directory().await?;
94
95        let mut lock_keys =
96            lock_keys![LockKey::object(store.store_object_id(), profile_dir.object_id())];
97        let mut old_id = INVALID_OBJECT_ID;
98        let mut transaction = loop {
99            let transaction = store.new_transaction(lock_keys, Options::default()).await?;
100            if let Some((id, descriptor, _)) = profile_dir.lookup(&self.name).await? {
101                ensure!(matches!(descriptor, ObjectDescriptor::File), FxfsError::Inconsistent);
102                if id == old_id {
103                    break transaction;
104                }
105                lock_keys = lock_keys![
106                    LockKey::object(store.store_object_id(), profile_dir.object_id()),
107                    LockKey::object(store.store_object_id(), id)
108                ];
109                old_id = id;
110            } else {
111                old_id = INVALID_OBJECT_ID;
112                break transaction;
113            }
114        };
115
116        store.remove_from_graveyard(&mut transaction, self.handle.object_id());
117        directory::replace_child_with_object(
118            &mut transaction,
119            Some((self.handle.object_id(), ObjectDescriptor::File)),
120            (&profile_dir, &self.name),
121            0,
122            false,
123            Timestamp::now(),
124        )
125        .await?;
126        transaction.commit().await?;
127
128        if old_id != INVALID_OBJECT_ID {
129            fs.ensure_local().graveyard().queue_tombstone_object(store.store_object_id(), old_id);
130        }
131
132        Ok(())
133    }
134}
135
136#[async_trait]
137impl RecordingHandle for FileRecordingHandle {
138    async fn append<'a>(&'a self, buf: BufferRef<'a>) -> Result<u64, Error> {
139        self.handle.write_or_append(None, buf).await.map_err(Into::into)
140    }
141
142    fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
143        self.handle.allocate_buffer(size)
144    }
145
146    fn block_size(&self) -> usize {
147        self.handle.block_size().get() as usize
148    }
149
150    async fn commit(self: Box<Self>) -> Result<(), Error> {
151        let store = self.volume.store();
152        self.commit_impl().await.inspect_err(|_| {
153            store
154                .filesystem()
155                .ensure_local()
156                .graveyard()
157                .queue_tombstone_object(store.store_object_id(), self.handle.object_id());
158        })
159    }
160
161    fn abort_cleanup(self: Box<Self>) {
162        self.volume
163            .store()
164            .filesystem()
165            .ensure_local()
166            .graveyard()
167            .queue_tombstone_object(self.volume.store().store_object_id(), self.handle.object_id());
168    }
169
170    fn filter_events(&self) -> bool {
171        true
172    }
173}
174
175/// A handle for storing the profile as an attribute to associate it with an object and tie it to
176/// that object's lifetime.
177pub struct AttributeRecordingHandle {
178    volume: Arc<FxVolume>,
179    handle: DataObjectHandle<FxVolume>,
180    /// Ensure that the associated node cannot be deleted while we write to the attribute.
181    _node: OpenedNode<dyn FxNode>,
182}
183
184impl AttributeRecordingHandle {
185    /// Create the profile handle to be stored as an attribute on the object to keep the profile
186    /// associated with the object and tie the profile lifetime to the object as well.
187    pub async fn new(node: OpenedNode<dyn FxNode>, volume: Arc<FxVolume>) -> Result<Self, Error> {
188        let store = volume.store();
189        let object_id = node.object_id();
190
191        // Prevent concurrent modification of the object while we add the recording attribute.
192        let mut transaction = store
193            .new_transaction(
194                lock_keys![LockKey::object(store.store_object_id(), object_id)],
195                Options::default(),
196            )
197            .await?;
198
199        transaction.add(
200            store.store_object_id(),
201            Mutation::replace_or_insert_object(
202                ObjectKey::attribute(
203                    object_id,
204                    AttributeId::PROFILE_RECORDING,
205                    AttributeKey::Attribute,
206                ),
207                ObjectValue::attribute(0, false),
208            ),
209        );
210        transaction.add(
211            store.store_object_id(),
212            Mutation::replace_or_insert_object(
213                ObjectKey::graveyard_attribute_entry(
214                    store.graveyard_directory_object_id(),
215                    object_id,
216                    AttributeId::PROFILE_RECORDING,
217                ),
218                ObjectValue::Some,
219            ),
220        );
221        transaction.commit().await?;
222
223        let handle = DataObjectHandle::new(
224            volume.clone(),
225            object_id,
226            false,
227            AttributeId::PROFILE_RECORDING,
228            0,
229            HandleOptions::default(),
230            false,
231            &[],
232        );
233
234        Ok(Self { volume, handle, _node: node })
235    }
236
237    pub async fn commit_impl(&self) -> Result<(), Error> {
238        let store = self.volume.store();
239        let mut transaction = store.new_transaction(lock_keys![], Options::default()).await?;
240
241        store.remove_attribute_from_graveyard(
242            &mut transaction,
243            self.handle.object_id(),
244            AttributeId::PROFILE_RECORDING,
245        );
246        transaction.commit().await?;
247        Ok(())
248    }
249}
250
251#[async_trait]
252impl RecordingHandle for AttributeRecordingHandle {
253    async fn append<'a>(
254        &'a self,
255        buf: storage_device::buffer::BufferRef<'a>,
256    ) -> Result<u64, Error> {
257        self.handle.write_or_append(None, buf).await.map_err(Into::into)
258    }
259
260    fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
261        self.handle.allocate_buffer(size)
262    }
263
264    fn block_size(&self) -> usize {
265        self.handle.block_size().get() as usize
266    }
267
268    async fn commit(self: Box<Self>) -> Result<(), Error> {
269        self.commit_impl().await.inspect_err(|_| {
270            self.abort_cleanup();
271        })
272    }
273
274    fn abort_cleanup(self: Box<Self>) {
275        // TODO(https://fxbug.dev/507875809): How is this going to be cleaned up if it fails part
276        // way? How will we know that it is invalid and should be replaced?
277        self.volume.store().filesystem().ensure_local().graveyard().queue_tombstone_attribute(
278            self.volume.store().store_object_id(),
279            self.handle.object_id(),
280            AttributeId::PROFILE_RECORDING,
281        );
282    }
283
284    fn filter_events(&self) -> bool {
285        false
286    }
287}
288
289trait RecordedVolume: Send + Sync + Sized + Unpin {
290    type IdType: std::fmt::Display + Ord + Send + Sized;
291    type NodeType: PagerBacked;
292    type MessageType: NodeMessage<IdType = Self::IdType>;
293
294    fn new(volume: Arc<FxVolume>) -> Self;
295
296    fn open(
297        &self,
298        id: Self::IdType,
299    ) -> impl std::future::Future<Output = Result<OpenedNode<Self::NodeType>, Error>> + Send;
300
301    /// Filters out open markers for files that may not be usable in the profile.
302    fn file_is_replayable(
303        &self,
304        id: &Self::IdType,
305    ) -> impl std::future::Future<Output = bool> + Send;
306
307    fn read_and_queue(
308        &self,
309        handle: Box<dyn ReadObjectHandle>,
310        sender: &async_channel::Sender<Request<Self::NodeType>>,
311        local_cache: &mut BTreeMap<Self::IdType, Option<OpenedNode<Self::NodeType>>>,
312    ) -> impl std::future::Future<Output = Result<(), Error>> + Send {
313        async move {
314            let mut io_buf = handle.allocate_buffer(IO_SIZE).await;
315            let block_size = handle.block_size().get() as usize;
316            let file_size = handle.get_size() as usize;
317            let mut offset = 0;
318            while offset < file_size {
319                let actual = handle
320                    .read_aligned(offset as u64, io_buf.as_mut())
321                    .await
322                    .map_err(|e| e.context(format!("Failed to read at offset: {}", offset)))?;
323                offset += actual;
324                let mut local_offset = 0;
325                let mut next_block = block_size;
326                let mut next_offset = size_of::<Self::MessageType>();
327                while next_offset <= actual {
328                    let data = io_buf.subslice(local_offset..next_offset);
329                    let msg = Self::MessageType::decode_from(data.as_ptr_slice());
330
331                    local_offset = next_offset;
332                    next_offset = local_offset + size_of::<Self::MessageType>();
333                    // Messages don't overlap block boundaries.
334                    if next_offset > next_block {
335                        local_offset = next_block;
336                        next_offset = local_offset + size_of::<Self::MessageType>();
337                        next_block += block_size;
338                    }
339
340                    // Ignore trailing zeroes. This is technically a valid entry but extremely
341                    // unlikely and will only break an optimization.
342                    if msg.is_zeroes() {
343                        break;
344                    }
345
346                    let file = match local_cache.entry(msg.id()) {
347                        Entry::Occupied(entry) => match entry.get() {
348                            Some(opened_file) => (*opened_file).clone(),
349                            // Found a cached error.
350                            None => continue,
351                        },
352                        Entry::Vacant(entry) => match self.open(msg.id()).await {
353                            Err(e) => {
354                                debug!("Failed to open object {} from profile: {:?}", msg.id(), e);
355                                // Cache the error.
356                                entry.insert(None);
357                                continue;
358                            }
359                            Ok(opened_file) => {
360                                let file_clone = opened_file.clone();
361                                entry.insert(Some(opened_file));
362                                file_clone
363                            }
364                        },
365                    };
366
367                    sender.send(Request { file, offset: msg.offset() }).await?;
368                }
369            }
370            Ok(())
371        }
372    }
373
374    fn record(
375        &self,
376        recording_handle: Box<dyn RecordingHandle>,
377        receiver: async_channel::Receiver<Vec<Self::MessageType>>,
378    ) -> impl std::future::Future<Output = Result<(), Error>> + Send {
379        // Ensure that this gets cleaned up if we cancel or fail anywhere.
380        let recording_handle = scopeguard::guard(recording_handle, |recording_handle| {
381            recording_handle.abort_cleanup();
382        });
383
384        async move {
385            let mut recorded_offsets = LinkedHashMap::<Self::MessageType, ()>::new();
386            let mut recorded_opens = BTreeMap::<Self::IdType, bool>::new();
387            let filter = recording_handle.filter_events();
388            while let Ok(buffer) = receiver.recv().await {
389                for message in buffer {
390                    if message.is_open_marker() {
391                        if filter {
392                            if let Entry::Vacant(entry) = recorded_opens.entry(message.id()) {
393                                let usable = self.file_is_replayable(entry.key()).await;
394                                entry.insert(usable);
395                            }
396                        }
397                    } else {
398                        recorded_offsets.insert(message, ());
399                    }
400                }
401            }
402
403            let block_size = recording_handle.block_size();
404            let mut offset = 0;
405            let mut io_buf = recording_handle.allocate_buffer(IO_SIZE).await;
406            let mut next_block = block_size;
407            while let Some((message, _)) = recorded_offsets.pop_front() {
408                // If a file opening was never recorded, or it is not usable drop the message.
409                if filter && !recorded_opens.get(&message.id()).copied().unwrap_or(false) {
410                    continue;
411                }
412
413                let mut next_offset = offset + size_of::<Self::MessageType>();
414                if next_offset > next_block {
415                    // Zero the remainder of the block. Stopping on block boundaries allows us to
416                    // resize the I/O without supporting reading/writing half messages to a buffer.
417                    io_buf.as_mut().subslice_mut(offset..next_block).fill(0);
418                    if next_block >= IO_SIZE {
419                        recording_handle
420                            .append(io_buf.as_ref())
421                            .await
422                            .context("Failed to write profile block")?;
423                        offset = 0;
424                        next_offset = size_of::<Self::MessageType>();
425                        next_block = block_size;
426                    } else {
427                        offset = next_block;
428                        next_offset = offset + size_of::<Self::MessageType>();
429                        next_block += block_size;
430                    }
431                }
432                message.encode_to(
433                    io_buf.as_mut().subslice_mut(offset..next_offset).as_mut_ptr_slice(),
434                );
435                offset = next_offset;
436            }
437            if offset > 0 {
438                io_buf.as_mut().subslice_mut(offset..next_block).fill(0);
439                recording_handle
440                    .append(io_buf.subslice(0..next_block))
441                    .await
442                    .context("Failed to write profile block")?;
443            }
444
445            std::mem::drop(io_buf);
446            // Defuse the cleanup.
447            let recording_handle = ScopeGuard::into_inner(recording_handle);
448            recording_handle.commit().await?;
449
450            Ok(())
451        }
452    }
453}
454
455struct BlobVolume {
456    volume: Arc<FxVolume>,
457    // Cache the open blob directory here. The Mutex is just to make this Send, but it is not
458    // actually used concurrently.
459    root_dir: Mutex<Option<Arc<BlobDirectory>>>,
460}
461
462impl RecordedVolume for BlobVolume {
463    type IdType = Hash;
464    type NodeType = FxBlob;
465    type MessageType = BlobMessage;
466
467    fn new(volume: Arc<FxVolume>) -> Self {
468        Self { volume, root_dir: Mutex::new(None) }
469    }
470
471    async fn open(&self, id: Self::IdType) -> Result<OpenedNode<Self::NodeType>, Error> {
472        let mut root_dir = self.root_dir.lock().await;
473        if root_dir.is_none() {
474            *root_dir = Some(
475                self.volume
476                    .get_or_load_node(
477                        self.volume.store().root_directory_object_id(),
478                        ObjectDescriptor::Directory,
479                        None,
480                    )
481                    .await?
482                    .into_any()
483                    .downcast::<BlobDirectory>()
484                    .map_err(|_| FxfsError::Inconsistent)?,
485            );
486        };
487        root_dir
488            .as_ref()
489            .unwrap()
490            .open_blob(&id.into())
491            .await?
492            .ok_or_else(|| FxfsError::NotFound.into())
493    }
494
495    async fn file_is_replayable(&self, _id: &Self::IdType) -> bool {
496        // There is nothing is filter out in blob volumes.
497        true
498    }
499}
500
501struct FileVolume {
502    volume: Arc<FxVolume>,
503}
504
505impl RecordedVolume for FileVolume {
506    type IdType = u64;
507    type NodeType = FxFile;
508    type MessageType = FileMessage;
509
510    fn new(volume: Arc<FxVolume>) -> Self {
511        Self { volume }
512    }
513
514    async fn open(&self, id: Self::IdType) -> Result<OpenedNode<Self::NodeType>, Error> {
515        self.volume.open_file_by_id(id).await
516    }
517
518    async fn file_is_replayable(&self, id: &Self::IdType) -> bool {
519        if self.volume.store().crypt().is_none() {
520            return true;
521        }
522        match self.volume.store().get_keys(*id).await {
523            // If any keys are not the volume key id, then the file may not be readable later.
524            // If there's more than one, then at least one is not the volume key.
525            Ok(keys)
526                if keys.is_empty()
527                    || (keys.len() == 1 && keys.first().unwrap().0 == VOLUME_DATA_KEY_ID) =>
528            {
529                true
530            }
531            _ => false,
532        }
533    }
534}
535
536trait NodeMessage: Message {
537    fn from_node_request(node: Arc<dyn FxNode>, offset: u64) -> Result<Self, Error>;
538}
539
540impl NodeMessage for BlobMessage {
541    fn from_node_request(node: Arc<dyn FxNode>, offset: u64) -> Result<Self, Error> {
542        match node.into_any().downcast::<FxBlob>() {
543            Ok(blob) => Ok(Self { id: blob.root(), offset }),
544            Err(_) => Err(anyhow!("Cannot record non-blob entry.")),
545        }
546    }
547}
548
549impl NodeMessage for FileMessage {
550    fn from_node_request(node: Arc<dyn FxNode>, offset: u64) -> Result<Self, Error> {
551        match node.into_any().downcast::<FxFile>() {
552            Ok(file) => Ok(Self { id: file.object_id(), offset }),
553            Err(_) => Err(anyhow!("Cannot record non-file entry")),
554        }
555    }
556}
557
558/// Takes messages to be written into the current profile. This should be dropped before the
559/// recording is stopped to ensure that all messages have been flushed to the writer thread.
560pub trait Recorder: Send + Sync {
561    /// Record a page in request, for the given identifier and offset.
562    fn record(&mut self, node: Arc<dyn FxNode>, offset: u64) -> Result<(), Error>;
563
564    /// Record file opens to gather what files were actually used during the recording.
565    fn record_open(&mut self, node: Arc<dyn FxNode>) -> Result<(), Error>;
566}
567
568struct RecorderImpl<T: NodeMessage> {
569    sender: async_channel::Sender<Vec<T>>,
570    buffer: Vec<T>,
571}
572
573impl<T: NodeMessage> RecorderImpl<T> {
574    fn new(sender: async_channel::Sender<Vec<T>>) -> Self {
575        Self { sender, buffer: Vec::with_capacity(MESSAGE_CHUNK_SIZE) }
576    }
577}
578
579impl<T: NodeMessage> Recorder for RecorderImpl<T> {
580    fn record(&mut self, node: Arc<dyn FxNode>, offset: u64) -> Result<(), Error> {
581        self.buffer.push(T::from_node_request(node, offset)?);
582        if self.buffer.len() >= MESSAGE_CHUNK_SIZE {
583            // try_send to avoid async await, we use an unbounded channel anyways so any failure
584            // here should only be if the channel is closed, which is permanent anyways.
585            self.sender.try_send(std::mem::replace(
586                &mut self.buffer,
587                Vec::with_capacity(MESSAGE_CHUNK_SIZE),
588            ))?;
589        }
590        RECORDED.fetch_add(1, Ordering::Relaxed);
591        Ok(())
592    }
593
594    fn record_open(&mut self, node: Arc<dyn FxNode>) -> Result<(), Error> {
595        self.record(node, FILE_OPEN_MARKER)
596    }
597}
598
599impl<T: NodeMessage> Drop for RecorderImpl<T> {
600    fn drop(&mut self) {
601        // Best effort sending what messages have already been queued.
602        if self.buffer.len() > 0 {
603            let buffer = std::mem::take(&mut self.buffer);
604            let _ = self.sender.try_send(buffer);
605        }
606    }
607}
608
609struct Request<P: PagerBacked> {
610    file: Arc<P>,
611    offset: u64,
612}
613
614struct ReplayState<T> {
615    replay_threads: future::Shared<BoxFuture<'static, ()>>,
616    _cache_task: fasync::Task<()>,
617    _phantom: PhantomData<T>,
618}
619
620impl<T: RecordedVolume> ReplayState<T> {
621    fn new(handle: Box<dyn ReadObjectHandle>, volume: Arc<FxVolume>, guard: ActiveGuard) -> Self {
622        let (sender, receiver) = async_channel::unbounded::<Request<T::NodeType>>();
623
624        // Create async_channel. An async thread reads and populates the channel, then N threads
625        // consume it and touch pages.
626        let mut replay_threads = Vec::with_capacity(REPLAY_THREADS);
627        for _ in 0..REPLAY_THREADS {
628            let receiver = receiver.clone();
629            // The replay threads can have references to files so we make sure they have a guard
630            // so that shutdown will wait till they have been joined.
631            let guard = guard.clone();
632            replay_threads.push(fasync::unblock(move || {
633                let _guard = guard;
634                Self::page_in_thread(receiver);
635            }));
636        }
637        let replay_threads = (Box::pin(async {
638            join_all(replay_threads).await;
639        }) as BoxFuture<'static, ()>)
640            .shared();
641
642        let scope = volume.scope().clone();
643        let cache_task = scope
644            .spawn({
645                // The replay threads hold active guards, so we must watch for cancellation.  When
646                // cancelled, we'll drop the sender which will cause the replay threads to drop
647                // their guards, which will allow shutdown to proceed.
648                async move {
649                    let mut task = pin!(
650                        async {
651                            // Hold the items in cache until replay is stopped. Optional as None
652                            // indicates that the file could not be opened, and we want to cache that
653                            // failure.
654                            let mut local_cache: BTreeMap<
655                                T::IdType,
656                                Option<OpenedNode<T::NodeType>>,
657                            > = BTreeMap::new();
658
659                            let volume_id = volume.id();
660
661                            if let Err(error) = T::new(volume)
662                                .read_and_queue(handle, &sender, &mut local_cache)
663                                .await
664                            {
665                                error!(error:?; "Failed to read back profile");
666                            }
667                            sender.close();
668
669                            info!(
670                                "Replay for volume {} opened {} of {} objects.",
671                                volume_id,
672                                local_cache.iter().filter(|(_, e)| e.is_some()).count(),
673                                local_cache.len()
674                            );
675
676                            // Keep the cache alive until dropped.
677                            let () = std::future::pending().await;
678                        }
679                        .fuse()
680                    );
681
682                    select! {
683                        _ = task => {}
684                        _ = guard.on_cancel().fuse() => {}
685                    }
686                }
687            })
688            .into();
689
690        Self { replay_threads, _cache_task: cache_task, _phantom: PhantomData }
691    }
692
693    fn page_in_thread(queue: async_channel::Receiver<Request<T::NodeType>>) {
694        let page_size = page_size().get();
695        while let Ok(request) = queue.recv_blocking() {
696            let res = request.file.vmo().op_range(zx::VmoOp::PREFETCH, request.offset, page_size);
697            if let Err(e) = res {
698                warn!("Failed to prefetch page: {:?}", e);
699            }
700            // If the volume is shutdown, the sender will be dropped.
701            if queue.sender_count() == 0 {
702                return;
703            }
704        }
705    }
706}
707
708/// Holds the current profile recording and/or replay state, and provides methods for state
709/// transitions.
710#[async_trait]
711pub trait ProfileState: Send + Sync {
712    /// Creates a new recording and returns the `Recorder` object to record to. The recording
713    /// finalizes when the associated `Recorder` is dropped.  Stops any recording currently in
714    /// progress.
715    fn record_new(
716        &mut self,
717        volume: &Arc<FxVolume>,
718        recording_handle: Box<dyn RecordingHandle>,
719    ) -> Box<dyn Recorder>;
720
721    /// Reads given handle to parse a profile and replay it by requesting pages via
722    /// ZX_VMO_OP_PREFETCH in blocking background threads. Stops any replay currently in progress.
723    fn replay_profile(
724        &mut self,
725        handle: Box<dyn ReadObjectHandle>,
726        volume: Arc<FxVolume>,
727        guard: ActiveGuard,
728    );
729
730    /// Waits for replay to finish, but does not drop the cache.  The cache will be dropped when
731    /// the ProfileState impl is dropped.  This is fine to call multiple times.
732    async fn wait_for_replay_to_finish(&mut self);
733
734    /// Waits for the recording to finish.
735    async fn wait_for_recording_to_finish(&mut self);
736}
737
738pub fn new_profile_state(is_blob: bool) -> Box<dyn ProfileState> {
739    if is_blob {
740        Box::new(ProfileStateImpl::<BlobVolume>::new())
741    } else {
742        Box::new(ProfileStateImpl::<FileVolume>::new())
743    }
744}
745
746struct ProfileStateImpl<T> {
747    recording: Option<fasync::CancelableJoinHandle<()>>,
748    replay: Option<ReplayState<T>>,
749}
750
751impl<T> ProfileStateImpl<T> {
752    fn new() -> Self {
753        Self { recording: None, replay: None }
754    }
755}
756
757#[async_trait]
758impl<T: RecordedVolume> ProfileState for ProfileStateImpl<T> {
759    fn record_new(
760        &mut self,
761        volume: &Arc<FxVolume>,
762        recording_handle: Box<dyn RecordingHandle>,
763    ) -> Box<dyn Recorder> {
764        let (sender, receiver) = async_channel::unbounded();
765        let volume = volume.clone();
766        // Cancel the previous recording (if any).
767        self.recording = None;
768        let scope = volume.scope().clone();
769        self.recording = Some(
770            scope
771                .spawn(async move {
772                    let recording = T::new(volume);
773                    if let Err(error) = recording.record(recording_handle, receiver).await {
774                        warn!(error:?; "Profile recording failed");
775                    }
776                })
777                .into(),
778        );
779        Box::new(RecorderImpl::new(sender))
780    }
781
782    fn replay_profile(
783        &mut self,
784        handle: Box<dyn ReadObjectHandle>,
785        volume: Arc<FxVolume>,
786        guard: ActiveGuard,
787    ) {
788        self.replay = Some(ReplayState::new(handle, volume, guard));
789    }
790
791    async fn wait_for_replay_to_finish(&mut self) {
792        if let Some(replay) = &mut self.replay {
793            replay.replay_threads.clone().await;
794        }
795    }
796
797    async fn wait_for_recording_to_finish(&mut self) {
798        if let Some(recording) = self.recording.take() {
799            let _ = recording.await;
800        }
801    }
802}
803
804#[cfg(test)]
805mod tests {
806    use super::{
807        AttributeRecordingHandle, BlobMessage, BlobVolume, FileMessage, FileRecordingHandle,
808        FileVolume, IO_SIZE, Message, RecordedVolume, RecordingHandle, Request, new_profile_state,
809    };
810    use crate::fuchsia::file::FxFile;
811    use crate::fuchsia::fxblob::blob::FxBlob;
812    use crate::fuchsia::fxblob::testing::{BlobFixture, new_blob_fixture, open_blob_fixture};
813    use crate::fuchsia::node::{FxNode, OpenedNode};
814    use crate::fuchsia::pager::PagerBacked;
815    use crate::fuchsia::testing::{TestFixture, TestFixtureOptions, open_file_checked};
816    use crate::fuchsia::volume::FxVolume;
817    use anyhow::{Error, ensure};
818    use async_trait::async_trait;
819    use delivery_blob::CompressionMode;
820    use event_listener::{Event, EventListener};
821    use fidl_fuchsia_fs_startup::CreateOptions;
822    use fidl_fuchsia_io as fio;
823    use fuchsia_async as fasync;
824    use fuchsia_hash::Hash;
825    use fuchsia_sync::Mutex;
826    use fxfs::errors::FxfsError;
827    use fxfs::object_handle::{ObjectHandle, ReadObjectHandle, WriteObjectHandle};
828    use fxfs::object_store::object_record::ObjectItem;
829    use fxfs::object_store::transaction::{LockKey, Options, lock_keys};
830    use fxfs::object_store::{
831        AttributeId, AttributeKey, DataObjectHandle, HandleOptions, ObjectDescriptor, ObjectKey,
832        ObjectStore, ObjectValue,
833    };
834    use std::collections::BTreeMap;
835    use std::mem::size_of;
836    use std::sync::Arc;
837    use std::time::Duration;
838    use storage_device::buffer::{BufferRef, MutableBufferRef};
839    use storage_device::buffer_allocator::{BufferAllocator, BufferFuture, BufferSource};
840    use storage_ptr_slice::MutPtrByteSlice;
841    use storage_units::BlockSize;
842
843    struct FakeReaderWriterInner {
844        data: Vec<u8>,
845        delays: Vec<EventListener>,
846    }
847
848    struct FakeReaderWriter {
849        allocator: BufferAllocator,
850        inner: Arc<Mutex<FakeReaderWriterInner>>,
851    }
852
853    const BLOCK_SIZE: usize = 4096;
854
855    impl FakeReaderWriter {
856        fn new() -> Self {
857            Self {
858                allocator: BufferAllocator::new(BLOCK_SIZE, BufferSource::new(IO_SIZE * 2)),
859                inner: Arc::new(Mutex::new(FakeReaderWriterInner {
860                    data: Vec::new(),
861                    delays: Vec::new(),
862                })),
863            }
864        }
865
866        fn push_delay(&self, delay: EventListener) {
867            self.inner.lock().delays.insert(0, delay);
868        }
869    }
870
871    impl ObjectHandle for FakeReaderWriter {
872        fn object_id(&self) -> u64 {
873            0
874        }
875
876        fn block_size(&self) -> BlockSize {
877            BlockSize::new(self.allocator.block_size() as u32).unwrap()
878        }
879
880        fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
881            self.allocator.allocate_buffer(size)
882        }
883    }
884
885    impl WriteObjectHandle for FakeReaderWriter {
886        async fn write_or_append(
887            &self,
888            offset: Option<u64>,
889            buf: BufferRef<'_>,
890        ) -> Result<u64, Error> {
891            // We only append for now.
892            assert!(offset.is_none());
893            let delay = self.inner.lock().delays.pop();
894            if let Some(delay) = delay {
895                delay.await;
896            }
897            // This relocking has a TOCTOU flavour, but it shouldn't matter for this application.
898            buf.append_to(&mut self.inner.lock().data);
899            Ok(buf.len() as u64)
900        }
901
902        async fn truncate(&self, _size: u64) -> Result<(), Error> {
903            unreachable!();
904        }
905
906        async fn flush(&self) -> Result<(), Error> {
907            unreachable!();
908        }
909    }
910
911    async fn write_file(fixture: &TestFixture, name: &str, data: &[u8]) -> u64 {
912        let root_dir = fixture.volume().root_dir();
913        let mut transaction = fixture
914            .volume()
915            .volume()
916            .store()
917            .new_transaction(
918                lock_keys![LockKey::object(
919                    fixture.volume().volume().store().store_object_id(),
920                    root_dir.object_id()
921                )],
922                Options::default(),
923            )
924            .await
925            .expect("Creating transaction for new file");
926        let id = root_dir
927            .directory()
928            .create_child_file(&mut transaction, name)
929            .await
930            .expect("Creating new_file")
931            .object_id();
932        transaction.commit().await.unwrap();
933        let file = open_file_checked(
934            fixture.root(),
935            name,
936            fio::PERM_READABLE | fio::PERM_WRITABLE | fio::Flags::PROTOCOL_FILE,
937            &Default::default(),
938        )
939        .await;
940        file.write(data).await.unwrap().expect("Writing file");
941        id
942    }
943
944    #[async_trait]
945    impl ReadObjectHandle for FakeReaderWriter {
946        async fn read_aligned(
947            &self,
948            offset: u64,
949            buf: MutableBufferRef<'_>,
950        ) -> Result<usize, Error> {
951            let block_size = self.block_size();
952            ensure!(block_size.is_aligned(offset), FxfsError::InvalidArgs);
953            ensure!(block_size.is_aligned(buf.len() as u64), FxfsError::InvalidArgs);
954            let delay = self.inner.lock().delays.pop();
955            if let Some(delay) = delay {
956                delay.await;
957            }
958            // This relocking has a TOCTOU flavour, but it shouldn't matter for this application.
959            let inner = self.inner.lock();
960            assert!(offset as usize <= inner.data.len());
961            let offset_end = std::cmp::min(offset as usize + buf.len(), inner.data.len());
962            let size = offset_end - offset as usize;
963            buf.subslice_mut(..size).copy_from_slice(&inner.data[offset as usize..offset_end]);
964            Ok(size)
965        }
966
967        fn get_size(&self) -> u64 {
968            self.inner.lock().data.len() as u64
969        }
970    }
971
972    const TEST_PROFILE_NAME: &str = "test_profile";
973
974    async fn get_test_profile_handle(volume: &Arc<FxVolume>) -> DataObjectHandle<FxVolume> {
975        let profile_dir = volume.get_profile_directory().await.unwrap();
976        ObjectStore::open_object(
977            volume,
978            profile_dir
979                .lookup(TEST_PROFILE_NAME)
980                .await
981                .expect("lookup failed")
982                .expect("not found")
983                .0,
984            HandleOptions::default(),
985            None,
986        )
987        .await
988        .unwrap()
989    }
990
991    async fn get_test_profile_contents(volume: &Arc<FxVolume>) -> Vec<u8> {
992        get_test_profile_handle(volume).await.contents(1024 * 1024).await.unwrap().to_vec()
993    }
994
995    #[fuchsia::test]
996    async fn test_recording_basic_blob() {
997        let fixture = new_blob_fixture().await;
998        {
999            let hash = fixture.write_blob(&[88u8], CompressionMode::Never).await;
1000            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1001
1002            let mut state = new_profile_state(true);
1003            let volume = fixture.volume().volume();
1004
1005            {
1006                // Drop recorder when finished writing to flush data.
1007                let handle =
1008                    FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1009                let mut recorder = state.record_new(volume, Box::new(handle));
1010                recorder.record(blob.clone(), 0).unwrap();
1011                recorder.record_open(blob).unwrap();
1012            }
1013
1014            state.wait_for_recording_to_finish().await;
1015
1016            assert_eq!(get_test_profile_contents(volume).await.len(), BLOCK_SIZE);
1017        }
1018        fixture.close().await;
1019    }
1020
1021    #[fuchsia::test]
1022    async fn test_recording_basic_file() {
1023        let fixture = TestFixture::new().await;
1024        {
1025            let id = write_file(&fixture, "foo", &[88u8]).await;
1026            let node = fixture
1027                .volume()
1028                .volume()
1029                .get_or_load_node(id, ObjectDescriptor::File, Some(fixture.volume().root_dir()))
1030                .await
1031                .unwrap();
1032
1033            let mut state = new_profile_state(false);
1034            let volume = fixture.volume().volume();
1035
1036            {
1037                // Drop recorder when finished writing to flush data.
1038                let handle =
1039                    FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1040                let mut recorder = state.record_new(volume, Box::new(handle));
1041                recorder.record(node.clone(), 0).unwrap();
1042                recorder.record_open(node).unwrap();
1043            }
1044            state.wait_for_recording_to_finish().await;
1045
1046            assert_eq!(get_test_profile_contents(volume).await.len(), BLOCK_SIZE);
1047        }
1048        fixture.close().await;
1049    }
1050
1051    #[fuchsia::test]
1052    async fn test_recording_filtered_without_open() {
1053        let fixture = new_blob_fixture().await;
1054        {
1055            let hash = fixture.write_blob(&[88u8], CompressionMode::Never).await;
1056            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1057
1058            let mut state = new_profile_state(true);
1059            let volume = fixture.volume().volume();
1060
1061            {
1062                // Drop recorder when finished writing to flush data.
1063                let handle =
1064                    FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1065                let mut recorder = state.record_new(volume, Box::new(handle));
1066                recorder.record(blob.clone(), 0).unwrap();
1067            }
1068            state.wait_for_recording_to_finish().await;
1069
1070            assert_eq!(get_test_profile_contents(volume).await.len(), 0);
1071        }
1072        fixture.close().await;
1073    }
1074
1075    #[fuchsia::test]
1076    async fn test_associated_recording_not_filtered_without_open() {
1077        let fixture = new_blob_fixture().await;
1078        {
1079            let hash = fixture.write_blob(&[88u8], CompressionMode::Never).await;
1080            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1081
1082            let mut state = new_profile_state(true);
1083            let volume = fixture.volume().volume();
1084
1085            {
1086                // Drop recorder when finished writing to flush data.
1087                let handle = AttributeRecordingHandle::new(
1088                    OpenedNode::new(blob.clone()).into_dyn(),
1089                    volume.clone(),
1090                )
1091                .await
1092                .unwrap();
1093                let mut recorder = state.record_new(volume, Box::new(handle));
1094                recorder.record(blob.clone(), 0).unwrap();
1095            }
1096            state.wait_for_recording_to_finish().await;
1097
1098            // The profile is stored as an attribute on the blob itself.
1099            // Let's get the attribute contents to see if it recorded the page.
1100            let key = ObjectKey::attribute(
1101                blob.object_id(),
1102                AttributeId::PROFILE_RECORDING,
1103                AttributeKey::Attribute,
1104            );
1105            let store = volume.store();
1106            let ObjectItem { value, .. } =
1107                store.tree().find(&key).await.unwrap().expect("profile attribute not found");
1108            let size = match value {
1109                ObjectValue::Attribute { size, .. } => size,
1110                _ => panic!("Expected Attribute value"),
1111            };
1112
1113            assert_eq!(size, BLOCK_SIZE as u64);
1114        }
1115        fixture.close().await;
1116    }
1117
1118    #[fuchsia::test]
1119    async fn test_attribute_recording_handle_prevents_purge_until_dropped() {
1120        let fixture = TestFixture::new_unencrypted().await;
1121        let root = fixture.root();
1122        let file_proxy = open_file_checked(
1123            root,
1124            "test_file",
1125            fio::Flags::FLAG_MAYBE_CREATE
1126                | fio::PERM_READABLE
1127                | fio::PERM_WRITABLE
1128                | fio::Flags::PROTOCOL_FILE,
1129            &Default::default(),
1130        )
1131        .await;
1132
1133        let object_id = file_proxy
1134            .get_attributes(fio::NodeAttributesQuery::ID)
1135            .await
1136            .unwrap()
1137            .expect("get_id")
1138            .1
1139            .id
1140            .expect("Missing id");
1141
1142        let volume = fixture.volume().volume();
1143        let node = volume.get_or_load_node(object_id, ObjectDescriptor::File, None).await.unwrap();
1144        let file = node.into_any().downcast::<FxFile>().unwrap();
1145        let opened_node = file.into_opened_node().unwrap();
1146
1147        let handle =
1148            AttributeRecordingHandle::new(opened_node.into_dyn(), volume.clone()).await.unwrap();
1149
1150        // Close file_proxy so only AttributeRecordingHandle holds the node open.
1151        drop(file_proxy);
1152
1153        // Unlink the file from root.
1154        root.unlink("test_file", &fio::UnlinkOptions::default())
1155            .await
1156            .unwrap()
1157            .expect("unlink failed");
1158
1159        // The file was unlinked, but because AttributeRecordingHandle holds OpenedNode,
1160        // it must not be tombstoned yet. Appending to the handle must succeed.
1161        {
1162            let mut buf = handle.allocate_buffer(handle.block_size()).await;
1163            buf.as_mut_ptr_slice().fill(1);
1164            handle.append(buf.as_ref()).await.expect("Append should succeed while handle is held");
1165        }
1166
1167        // Now drop the recording handle (via commit).
1168        handle.commit_impl().await.expect("commit_impl failed");
1169        drop(handle);
1170
1171        fixture.close().await;
1172    }
1173
1174    #[fuchsia::test]
1175    async fn test_recording_blob_more_than_block() {
1176        let mut state = new_profile_state(true);
1177
1178        let fixture = new_blob_fixture().await;
1179        assert_eq!(BLOCK_SIZE as u64, fixture.fs().block_size());
1180        let message_count =
1181            (fixture.fs().block_size().get() as usize / size_of::<BlobMessage>()) + 1;
1182        let hash;
1183        let volume = fixture.volume().volume();
1184
1185        {
1186            hash = fixture.write_blob(&[88u8], CompressionMode::Never).await;
1187            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1188            // Drop recorder when finished writing to flush data.
1189            let handle = FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1190            let mut recorder = state.record_new(volume, Box::new(handle));
1191            recorder.record_open(blob.clone()).unwrap();
1192            for i in 0..message_count {
1193                recorder.record(blob.clone(), 4096 * i as u64).unwrap();
1194            }
1195        }
1196        state.wait_for_recording_to_finish().await;
1197
1198        assert_eq!(get_test_profile_contents(volume).await.len(), BLOCK_SIZE * 2);
1199
1200        let mut local_cache: BTreeMap<Hash, Option<OpenedNode<FxBlob>>> = BTreeMap::new();
1201        let (sender, receiver) = async_channel::unbounded::<Request<FxBlob>>();
1202
1203        let volume = fixture.volume().volume().clone();
1204        let task = fasync::Task::spawn(async move {
1205            let handle = Box::new(get_test_profile_handle(&volume).await);
1206            let blob = BlobVolume::new(volume);
1207            blob.read_and_queue(handle, &sender, &mut local_cache).await.unwrap();
1208        });
1209
1210        let mut recv_count = 0;
1211        while let Ok(msg) = receiver.recv().await {
1212            assert_eq!(msg.file.root(), hash);
1213            assert_eq!(msg.offset, 4096 * recv_count);
1214            recv_count += 1;
1215        }
1216        task.await;
1217        assert_eq!(recv_count, message_count as u64);
1218
1219        fixture.close().await;
1220    }
1221
1222    #[fuchsia::test]
1223    async fn test_recording_file_more_than_block() {
1224        let mut state = new_profile_state(false);
1225
1226        let fixture = TestFixture::new().await;
1227        assert_eq!(BLOCK_SIZE as u64, fixture.fs().block_size());
1228        let message_count =
1229            (fixture.fs().block_size().get() as usize / size_of::<FileMessage>()) + 1;
1230        let id;
1231        let volume = fixture.volume().volume();
1232        {
1233            id = write_file(&fixture, "foo", &[88u8]).await;
1234            let node = volume
1235                .get_or_load_node(id, ObjectDescriptor::File, Some(fixture.volume().root_dir()))
1236                .await
1237                .unwrap();
1238            // Drop recorder when finished writing to flush data.
1239            let handle = FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1240            let mut recorder = state.record_new(volume, Box::new(handle));
1241            recorder.record_open(node.clone()).unwrap();
1242            for i in 0..message_count {
1243                recorder.record(node.clone(), 4096 * i as u64).unwrap();
1244            }
1245        }
1246        state.wait_for_recording_to_finish().await;
1247
1248        assert_eq!(get_test_profile_contents(volume).await.len(), BLOCK_SIZE * 2);
1249
1250        let mut local_cache: BTreeMap<u64, Option<OpenedNode<FxFile>>> = BTreeMap::new();
1251        let (sender, receiver) = async_channel::unbounded::<Request<FxFile>>();
1252
1253        let volume = fixture.volume().volume().clone();
1254        let task = fasync::Task::spawn(async move {
1255            let handle = Box::new(get_test_profile_handle(&volume).await);
1256            let file = FileVolume::new(volume);
1257            file.read_and_queue(handle, &sender, &mut local_cache).await.unwrap();
1258        });
1259
1260        let mut recv_count = 0;
1261        while let Ok(msg) = receiver.recv().await {
1262            assert_eq!(msg.file.object_id(), id);
1263            assert_eq!(msg.offset, 4096 * recv_count);
1264            recv_count += 1;
1265        }
1266        task.await;
1267        assert_eq!(recv_count, message_count as u64);
1268
1269        fixture.close().await;
1270    }
1271
1272    #[fuchsia::test]
1273    async fn test_recording_more_than_io_size() {
1274        let fixture = new_blob_fixture().await;
1275
1276        {
1277            let mut state = new_profile_state(true);
1278            let message_count = (IO_SIZE as usize / size_of::<BlobMessage>()) + 1;
1279            let hash;
1280            let volume = fixture.volume().volume();
1281            {
1282                hash = fixture.write_blob(&[88u8], CompressionMode::Never).await;
1283                let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1284                // Drop recorder when finished writing to flush data.
1285                let handle =
1286                    FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1287                let mut recorder = state.record_new(volume, Box::new(handle));
1288                recorder.record_open(blob.clone()).unwrap();
1289                for i in 0..message_count {
1290                    recorder.record(blob.clone(), 4096 * i as u64).unwrap();
1291                }
1292            }
1293            state.wait_for_recording_to_finish().await;
1294            assert_eq!(get_test_profile_contents(volume).await.len(), IO_SIZE + BLOCK_SIZE);
1295
1296            let mut local_cache: BTreeMap<Hash, Option<OpenedNode<FxBlob>>> = BTreeMap::new();
1297            let (sender, receiver) = async_channel::unbounded::<Request<FxBlob>>();
1298
1299            let volume = volume.clone();
1300            let task = fasync::Task::spawn(async move {
1301                let handle = Box::new(get_test_profile_handle(&volume).await);
1302                let blob = BlobVolume::new(volume);
1303                blob.read_and_queue(handle, &sender, &mut local_cache).await.unwrap();
1304            });
1305
1306            let mut recv_count = 0;
1307            while let Ok(msg) = receiver.recv().await {
1308                assert_eq!(msg.file.root(), hash);
1309                assert_eq!(msg.offset, 4096 * recv_count);
1310                recv_count += 1;
1311            }
1312            task.await;
1313            assert_eq!(recv_count, message_count as u64);
1314        }
1315
1316        fixture.close().await;
1317    }
1318
1319    #[fuchsia::test]
1320    async fn test_replay_profile_blob() {
1321        // Create all the files that we need first, then restart the filesystem to clear cache.
1322        let mut state = new_profile_state(true);
1323
1324        let mut hashes = Vec::new();
1325
1326        let fixture = new_blob_fixture().await;
1327        {
1328            assert_eq!(BLOCK_SIZE as u64, fixture.fs().block_size());
1329            let message_count =
1330                (fixture.fs().block_size().get() as usize / size_of::<BlobMessage>()) + 1;
1331
1332            let volume = fixture.volume().volume();
1333            let handle = FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1334            let mut recorder = state.record_new(volume, Box::new(handle));
1335            // Page in the zero offsets only to avoid readahead strangeness.
1336            for i in 0..message_count {
1337                let hash =
1338                    fixture.write_blob(i.to_string().as_bytes(), CompressionMode::Never).await;
1339                let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1340                recorder.record_open(blob.clone()).unwrap();
1341                hashes.push(hash);
1342                recorder.record(blob.clone(), 0).unwrap();
1343            }
1344        };
1345        let device = fixture.close().await;
1346        device.ensure_unique();
1347        state.wait_for_recording_to_finish().await;
1348
1349        device.reopen(false);
1350        let fixture = open_blob_fixture(device).await;
1351        {
1352            // Need to get the root vmo to check committed bytes.
1353            // Ensure that nothing is paged in right now.
1354            for hash in &hashes {
1355                let blob = fixture.get_blob(*hash).await.expect("Opening blob");
1356                assert_eq!(blob.vmo().info().unwrap().committed_bytes, 0);
1357            }
1358
1359            let volume = fixture.volume().volume();
1360            state.replay_profile(
1361                Box::new(get_test_profile_handle(volume).await),
1362                volume.clone(),
1363                volume.scope().try_active_guard().unwrap(),
1364            );
1365
1366            // Await all data being played back by checking that things have paged in.
1367            for hash in &hashes {
1368                let blob = fixture.get_blob(*hash).await.expect("Opening blob");
1369                while blob.vmo().info().unwrap().committed_bytes == 0 {
1370                    fasync::Timer::new(Duration::from_millis(25)).await;
1371                }
1372            }
1373        }
1374        fixture.close().await;
1375    }
1376
1377    #[fuchsia::test]
1378    async fn test_replay_profile_file() {
1379        // Create all the files that we need first, then restart the filesystem to clear cache.
1380        let mut state = new_profile_state(false);
1381
1382        let mut ids = Vec::new();
1383
1384        let fixture = TestFixture::new().await;
1385        {
1386            assert_eq!(BLOCK_SIZE as u64, fixture.fs().block_size());
1387            let message_count =
1388                (fixture.fs().block_size().get() as usize / size_of::<FileMessage>()) + 1;
1389
1390            let volume = fixture.volume().volume();
1391            let handle = FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1392            let mut recorder = state.record_new(volume, Box::new(handle));
1393            // Page in the zero offsets only to avoid readahead strangeness.
1394            for i in 0..message_count {
1395                let id = write_file(&fixture, &i.to_string(), &[88u8]).await;
1396                let node = fixture
1397                    .volume()
1398                    .volume()
1399                    .get_or_load_node(id, ObjectDescriptor::File, Some(fixture.volume().root_dir()))
1400                    .await
1401                    .unwrap();
1402                recorder.record_open(node.clone()).unwrap();
1403                ids.push(id);
1404                recorder.record(node.clone(), 0).unwrap();
1405            }
1406        };
1407        let device = fixture.close().await;
1408        device.ensure_unique();
1409        state.wait_for_recording_to_finish().await;
1410
1411        device.reopen(false);
1412        let fixture = TestFixture::open(
1413            device,
1414            TestFixtureOptions { encrypted: true, format: false, ..Default::default() },
1415        )
1416        .await;
1417        {
1418            // Ensure that nothing is paged in right now.
1419            for id in &ids {
1420                let file = fixture
1421                    .volume()
1422                    .volume()
1423                    .get_or_load_node(
1424                        *id,
1425                        ObjectDescriptor::File,
1426                        Some(fixture.volume().root_dir()),
1427                    )
1428                    .await
1429                    .unwrap()
1430                    .into_any()
1431                    .downcast::<FxFile>()
1432                    .unwrap();
1433                assert_eq!(file.vmo().info().unwrap().committed_bytes, 0);
1434            }
1435
1436            let volume = fixture.volume().volume();
1437            state.replay_profile(
1438                Box::new(get_test_profile_handle(volume).await),
1439                volume.clone(),
1440                volume.scope().try_active_guard().unwrap(),
1441            );
1442
1443            // Await all data being played back by checking that things have paged in.
1444            for id in &ids {
1445                let file = fixture
1446                    .volume()
1447                    .volume()
1448                    .get_or_load_node(
1449                        *id,
1450                        ObjectDescriptor::File,
1451                        Some(fixture.volume().root_dir()),
1452                    )
1453                    .await
1454                    .unwrap()
1455                    .into_any()
1456                    .downcast::<FxFile>()
1457                    .unwrap();
1458                while file.vmo().info().unwrap().committed_bytes == 0 {
1459                    fasync::Timer::new(Duration::from_millis(25)).await;
1460                }
1461            }
1462            state.wait_for_recording_to_finish().await;
1463        }
1464        fixture.close().await;
1465    }
1466
1467    #[fuchsia::test]
1468    async fn test_recording_during_replay() {
1469        let mut state = new_profile_state(true);
1470
1471        let hash;
1472        let first_recording;
1473        let fixture = new_blob_fixture().await;
1474        let volume = fixture.volume().volume();
1475
1476        // First make a simple recording.
1477        {
1478            let handle = FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1479            let mut recorder = state.record_new(volume, Box::new(handle));
1480            hash = fixture.write_blob(&[0, 1, 2, 3], CompressionMode::Never).await;
1481            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1482            recorder.record_open(blob.clone()).unwrap();
1483            recorder.record(blob.clone(), 0).unwrap();
1484        }
1485
1486        state.wait_for_recording_to_finish().await;
1487        first_recording = get_test_profile_contents(volume).await;
1488        assert_ne!(first_recording.len(), 0);
1489        let device = fixture.close().await;
1490        device.ensure_unique();
1491
1492        device.reopen(false);
1493        let fixture = open_blob_fixture(device).await;
1494
1495        {
1496            // Need to get the root vmo to check committed bytes.
1497            // Ensure that nothing is paged in right now.
1498            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1499            assert_eq!(blob.vmo().info().unwrap().committed_bytes, 0);
1500
1501            // Start recording
1502            let volume = fixture.volume().volume();
1503            let handle = FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1504            let mut recorder = state.record_new(volume, Box::new(handle));
1505            recorder.record(blob.clone(), 4096).unwrap();
1506
1507            // Replay the original recording.
1508            let volume = fixture.volume().volume();
1509            state.replay_profile(
1510                Box::new(get_test_profile_handle(volume).await),
1511                volume.clone(),
1512                volume.scope().try_active_guard().unwrap(),
1513            );
1514
1515            // Await all data being played back by checking that things have paged in.
1516            {
1517                let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1518                while blob.vmo().info().unwrap().committed_bytes == 0 {
1519                    fasync::Timer::new(Duration::from_millis(25)).await;
1520                }
1521            }
1522
1523            // Record the open after the replay. Needs both the before and after action to
1524            // capture anything ensuring that the two procedures overlapped.
1525            recorder.record_open(blob.clone()).unwrap();
1526        }
1527
1528        state.wait_for_recording_to_finish().await;
1529
1530        let volume = fixture.volume().volume();
1531        let second_recording = get_test_profile_contents(volume).await;
1532        assert_ne!(second_recording.len(), 0);
1533        assert_ne!(&second_recording, &first_recording);
1534
1535        fixture.close().await;
1536    }
1537
1538    // Doesn't ensure that anything reads back properly, just that everything shuts down when
1539    // stopped early.
1540    #[fuchsia::test]
1541    async fn test_replay_profile_stop_reading_early() {
1542        let mut state = new_profile_state(true);
1543        let fixture = new_blob_fixture().await;
1544
1545        {
1546            let volume = fixture.volume().volume();
1547
1548            // Create the file that we need first.
1549            let message;
1550            {
1551                let hash = fixture.write_blob(&[0, 1, 2, 3], CompressionMode::Never).await;
1552                let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1553                message = BlobMessage { id: blob.root(), offset: 0 };
1554            }
1555            state.wait_for_recording_to_finish().await;
1556
1557            // Make a profile long enough to require 2 reads.
1558            let replay_handle = Box::new(FakeReaderWriter::new());
1559            let mut buff = vec![0u8; IO_SIZE * 2];
1560            message.encode_to(MutPtrByteSlice::from(&mut buff[0..size_of::<BlobMessage>()]));
1561            message.encode_to(MutPtrByteSlice::from(
1562                &mut buff[IO_SIZE..IO_SIZE + size_of::<BlobMessage>()],
1563            ));
1564
1565            replay_handle.inner.lock().data = buff;
1566            let delay1 = Event::new();
1567            replay_handle.push_delay(delay1.listen());
1568            let delay2 = Event::new();
1569            replay_handle.push_delay(delay2.listen());
1570
1571            state.replay_profile(
1572                replay_handle,
1573                volume.clone(),
1574                volume.scope().try_active_guard().unwrap(),
1575            );
1576
1577            // Delay the first read long enough so that the stop can be triggered during it.
1578            fasync::Task::spawn(async move {
1579                // Let the profiler wait on this a little.
1580                fasync::Timer::new(Duration::from_millis(100)).await;
1581                delay1.notify(usize::MAX);
1582            })
1583            .detach();
1584        }
1585
1586        // The reader should block indefinitely (we never notify delay2), but that shouldn't block
1587        // termination.
1588        fixture.close().await;
1589    }
1590
1591    #[fuchsia::test]
1592    async fn test_replay_blob_missing() {
1593        let fixture = new_blob_fixture().await;
1594        // Create the blob that comes after the missing blob. Ensure it still gets
1595        // recorded.
1596        let hash = fixture.write_blob(&[0, 1, 2, 3], CompressionMode::Never).await;
1597        let mut buff = vec![0u8; IO_SIZE];
1598        {
1599            // First encode the blob that is missing. Just make it up. This will be skipped during
1600            // replay.
1601            {
1602                let message = BlobMessage { id: [42u8; 32].into(), offset: 0 };
1603                message.encode_to(MutPtrByteSlice::from(&mut buff[0..size_of::<BlobMessage>()]));
1604            }
1605
1606            // Create the blob that won't be missing and encode that.
1607            {
1608                let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1609                let message = BlobMessage { id: blob.root(), offset: 0 };
1610                message.encode_to(MutPtrByteSlice::from(
1611                    &mut buff[size_of::<BlobMessage>()..(size_of::<BlobMessage>() * 2)],
1612                ));
1613            }
1614        }
1615        let device = fixture.close().await;
1616        device.ensure_unique();
1617
1618        device.reopen(false);
1619        let fixture = open_blob_fixture(device).await;
1620        {
1621            let mut state = new_profile_state(true);
1622            let volume = fixture.volume().volume();
1623
1624            let replay_handle = Box::new(FakeReaderWriter::new());
1625            replay_handle.inner.lock().data = buff;
1626
1627            state.replay_profile(
1628                replay_handle,
1629                volume.clone(),
1630                volume.scope().try_active_guard().unwrap(),
1631            );
1632
1633            // Wait for the replay to populate the page.
1634            let blob = fixture.get_blob((*hash).into()).await.expect("Opening blob");
1635            while blob.vmo().info().unwrap().committed_bytes == 0 {
1636                fasync::Timer::new(Duration::from_millis(25)).await;
1637            }
1638        }
1639        fixture.close().await;
1640    }
1641
1642    #[fuchsia::test]
1643    async fn test_replay_file_missing_or_tombstoned() {
1644        let fixture = TestFixture::new().await;
1645        let mut buff = vec![0u8; IO_SIZE];
1646        // Create the blob that comes after the missing blob. Ensure it still gets
1647        // recorded.
1648        let remaining_file_id;
1649        let tombstoned_file_id;
1650        // First encode the file that is missing.
1651        {
1652            let id = write_file(&fixture, "foo", &[1, 2, 3, 4]).await;
1653            let message = FileMessage { id, offset: 0 };
1654            message.encode_to(MutPtrByteSlice::from(&mut buff[0..size_of::<FileMessage>()]));
1655        }
1656        // Remove the file now.
1657        fixture
1658            .root()
1659            .unlink("foo", &fio::UnlinkOptions::default())
1660            .await
1661            .unwrap()
1662            .expect("Unlinking");
1663
1664        // Encode the file that will be tombstoned during replay.
1665        {
1666            tombstoned_file_id = write_file(&fixture, "bar", &[1, 2, 3, 4]).await;
1667            let message = FileMessage { id: tombstoned_file_id, offset: 0 };
1668            message.encode_to(MutPtrByteSlice::from(
1669                &mut buff[size_of::<FileMessage>()..(size_of::<FileMessage>() * 2)],
1670            ));
1671        }
1672
1673        // Encode the file that will remain and be replayed last.
1674        {
1675            remaining_file_id = write_file(&fixture, "baz", &[1, 2, 3, 4]).await;
1676            let message = FileMessage { id: remaining_file_id, offset: 0 };
1677            message.encode_to(MutPtrByteSlice::from(
1678                &mut buff[(size_of::<FileMessage>() * 2)..(size_of::<FileMessage>() * 3)],
1679            ));
1680        }
1681        let device = fixture.close().await;
1682        device.ensure_unique();
1683
1684        device.reopen(false);
1685        let fixture =
1686            TestFixture::open(device, TestFixtureOptions { format: false, ..Default::default() })
1687                .await;
1688        {
1689            // Get a ref to the Arc on the file, then unlink it. Since the open count is zero it
1690            // should get marked for tombstone right away.
1691            let tombstoned_file = fixture
1692                .volume()
1693                .volume()
1694                .get_or_load_node(tombstoned_file_id, ObjectDescriptor::File, None)
1695                .await
1696                .expect("Opening file object")
1697                .into_any()
1698                .downcast::<FxFile>()
1699                .unwrap();
1700            fixture
1701                .root()
1702                .unlink("bar", &fio::UnlinkOptions::default())
1703                .await
1704                .unwrap()
1705                .expect("Unlinking");
1706
1707            let mut state = new_profile_state(false);
1708            let volume = fixture.volume().volume();
1709
1710            let replay_handle = Box::new(FakeReaderWriter::new());
1711            replay_handle.inner.lock().data = buff;
1712
1713            state.replay_profile(
1714                replay_handle,
1715                volume.clone(),
1716                volume.scope().try_active_guard().unwrap(),
1717            );
1718
1719            // Wait for the replay to populate the page.
1720            let remaining_file = fixture
1721                .volume()
1722                .volume()
1723                .get_or_load_node(remaining_file_id, ObjectDescriptor::File, None)
1724                .await
1725                .expect("Opening file object")
1726                .into_any()
1727                .downcast::<FxFile>()
1728                .unwrap();
1729            while remaining_file.vmo().info().unwrap().committed_bytes == 0 {
1730                fasync::Timer::new(Duration::from_millis(25)).await;
1731            }
1732
1733            // The tombstoned file should not have anything committed because it shouldn't be able
1734            // to open.
1735            assert_eq!(tombstoned_file.vmo().info().unwrap().committed_bytes, 0);
1736        }
1737        fixture.close().await;
1738    }
1739
1740    #[fuchsia::test]
1741    async fn test_replay_file_unlinked_while_open_is_not_replayed() {
1742        let fixture = TestFixture::new().await;
1743        let mut buff = vec![0u8; IO_SIZE];
1744
1745        let unlinked_file_id = write_file(&fixture, "unlinked", &[1, 2, 3, 4]).await;
1746        let message = FileMessage { id: unlinked_file_id, offset: 0 };
1747        message.encode_to(MutPtrByteSlice::from(&mut buff[0..size_of::<FileMessage>()]));
1748
1749        let remaining_file_id = write_file(&fixture, "remaining", &[5, 6, 7, 8]).await;
1750        let message = FileMessage { id: remaining_file_id, offset: 0 };
1751        message.encode_to(MutPtrByteSlice::from(
1752            &mut buff[size_of::<FileMessage>()..(size_of::<FileMessage>() * 2)],
1753        ));
1754
1755        let device = fixture.close().await;
1756        device.ensure_unique();
1757
1758        device.reopen(false);
1759        let fixture =
1760            TestFixture::open(device, TestFixtureOptions { format: false, ..Default::default() })
1761                .await;
1762        {
1763            // Open the file so that it has an active connection and open count > 0.
1764            let file_proxy = open_file_checked(
1765                fixture.root(),
1766                "unlinked",
1767                fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
1768                &Default::default(),
1769            )
1770            .await;
1771
1772            // Unlink it while open. It is now in the graveyard, but still held open.
1773            fixture
1774                .root()
1775                .unlink("unlinked", &fio::UnlinkOptions::default())
1776                .await
1777                .unwrap()
1778                .expect("Unlinking failed");
1779
1780            let store = fixture.volume().volume().store();
1781            assert!(
1782                matches!(
1783                    store
1784                        .tree()
1785                        .find_value(&ObjectKey::graveyard_entry(
1786                            store.graveyard_directory_object_id(),
1787                            unlinked_file_id,
1788                        ))
1789                        .await
1790                        .unwrap(),
1791                    Some(ObjectValue::Some)
1792                ),
1793                "File should be in graveyard awaiting purge"
1794            );
1795
1796            let mut state = new_profile_state(false);
1797            let volume = fixture.volume().volume();
1798
1799            let replay_handle = Box::new(FakeReaderWriter::new());
1800            replay_handle.inner.lock().data = buff;
1801
1802            state.replay_profile(
1803                replay_handle,
1804                volume.clone(),
1805                volume.scope().try_active_guard().unwrap(),
1806            );
1807
1808            // Wait for remaining_file to be replayed and paged in.
1809            let remaining_file = fixture
1810                .volume()
1811                .volume()
1812                .get_or_load_node(remaining_file_id, ObjectDescriptor::File, None)
1813                .await
1814                .expect("Opening remaining file")
1815                .into_any()
1816                .downcast::<FxFile>()
1817                .unwrap();
1818            while remaining_file.vmo().info().unwrap().committed_bytes == 0 {
1819                fasync::Timer::new(Duration::from_millis(25)).await;
1820            }
1821
1822            // The unlinked file in the graveyard should NOT have been replayed/committed.
1823            let node = fixture
1824                .volume()
1825                .volume()
1826                .get_or_load_node(unlinked_file_id, ObjectDescriptor::File, None)
1827                .await
1828                .unwrap();
1829            let unlinked_file = node.into_any().downcast::<FxFile>().unwrap();
1830            assert_eq!(unlinked_file.vmo().info().unwrap().committed_bytes, 0);
1831
1832            drop(file_proxy);
1833        }
1834        fixture.close().await;
1835    }
1836
1837    #[fuchsia::test]
1838    async fn test_recording_stopped_by_scope_shutdown() {
1839        let fixture = TestFixture::new().await;
1840        {
1841            let volumes_directory = fixture.volumes_directory();
1842            let mut state = new_profile_state(false);
1843            // The recorder outlives the shutdown here, to show that we stopped the recording due to
1844            // scope shutdown.
1845            let (store_id, _recorder) = {
1846                let volume_and_root = volumes_directory
1847                    .create_and_mount_volume("other_volume", None, false, CreateOptions::default())
1848                    .await
1849                    .unwrap();
1850                let volume = volume_and_root.into_volume();
1851
1852                let handle =
1853                    FileRecordingHandle::new(TEST_PROFILE_NAME, volume.clone()).await.unwrap();
1854                (volume.store().store_object_id(), state.record_new(&volume, Box::new(handle)))
1855            };
1856            volumes_directory.lock().await.unmount(store_id).await.expect("Unmounting");
1857            state.wait_for_recording_to_finish().await;
1858
1859            let volume_and_root =
1860                volumes_directory.mount_volume("other_volume", None, false).await.unwrap();
1861            let volume = volume_and_root.into_volume();
1862            let profile_dir = volume.get_profile_directory().await.unwrap();
1863            assert!(profile_dir.lookup(TEST_PROFILE_NAME).await.unwrap().is_none());
1864        }
1865        fixture.close().await;
1866    }
1867}