Skip to main content

fuchsia_storage_benchmarks/
block_devices.rs

1// Copyright 2022 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 async_trait::async_trait;
6use block_matcher::{Guid, create_random_guid};
7use fidl::endpoints::{
8    DiscoverableProtocolMarker as _, Proxy, create_proxy, create_request_stream,
9};
10use fidl_fuchsia_fs_startup::{CreateOptions, MountOptions};
11use fidl_fuchsia_io as fio;
12use fidl_fuchsia_storage_block::BlockMarker;
13use fidl_fuchsia_storage_partitions as fpartitions;
14use fs_management::Fvm;
15use fs_management::filesystem::{
16    BlockConnector, DirBasedBlockConnector, ServingMultiVolumeFilesystem,
17};
18use fuchsia_async as fasync;
19use fuchsia_component::client::{Service, connect_to_protocol, connect_to_protocol_at_dir_root};
20use std::sync::Arc;
21use storage_benchmarks::block_device::BlockDevice;
22use storage_benchmarks::{BlockDeviceConfig, BlockDeviceFactory};
23
24const BENCHMARK_FVM_SIZE_BYTES: u64 = 160 * 1024 * 1024;
25// 8MiB is the default slice size; use it so the test FVM partition matches the performance of the
26// system FVM partition (so they are interchangeable).
27// Note that this only affects the performance of minfs and blobfs, since these two filesystems are
28// the only ones that dynamically allocate from FVM.
29const BENCHMARK_FVM_SLICE_SIZE_BYTES: u64 = 8 * 1024 * 1024;
30
31// On systems which don't have FVM (i.e. Fxblob), we create an FVM partition the test can use, with
32// this GUID.  See connect_to_test_fvm for details.
33
34const BENCHMARK_TYPE_GUID: &Guid = &[
35    0x67, 0x45, 0x23, 0x01, 0xab, 0x89, 0xef, 0xcd, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
36];
37const BENCHMARK_VOLUME_NAME: &str = "benchmark";
38
39/// Returns the exposed directory of the volume, as well as the task running Crypt for the volume
40/// (if configured in `config`).
41pub async fn create_fvm_volume(
42    fvm: &fidl_fuchsia_fs_startup::VolumesProxy,
43    instance_guid: [u8; 16],
44    config: &BlockDeviceConfig,
45) -> (fio::DirectoryProxy, Option<fasync::Task<()>>) {
46    loop {
47        let (crypt, crypt_task) = if config.use_zxcrypt {
48            let (crypt, stream) = create_request_stream::<fidl_fuchsia_fxfs::CryptMarker>();
49            let task = fasync::Task::spawn(async {
50                if let Err(err) =
51                    zxcrypt_crypt::run_crypt_service(crypt_policy::Policy::Null, stream).await
52                {
53                    log::error!(err:?; "Crypt service failure");
54                }
55            });
56            (Some(crypt), Some(task))
57        } else {
58            (None, None)
59        };
60
61        let (volume_dir, server_end) = create_proxy::<fio::DirectoryMarker>();
62        let res = fvm
63            .create(
64                BENCHMARK_VOLUME_NAME,
65                server_end,
66                CreateOptions {
67                    initial_size: config.volume_size,
68                    type_guid: Some(BENCHMARK_TYPE_GUID.clone()),
69                    guid: Some(instance_guid),
70                    ..Default::default()
71                },
72                MountOptions { crypt, ..Default::default() },
73            )
74            .await
75            .expect("FIDL error")
76            .map_err(zx::Status::err_from_raw);
77
78        // If the FVM already exists, remove it and try again.
79        if res == Err(zx::Status::ALREADY_EXISTS) {
80            fvm.remove(BENCHMARK_VOLUME_NAME)
81                .await
82                .expect("FIDL error")
83                .map_err(zx::Status::err_from_raw)
84                .expect("Failed to remove volume");
85            continue;
86        }
87
88        res.expect("Failed to create volume");
89        return (volume_dir, crypt_task);
90    }
91}
92
93/// A factory for volumes which the benchmarks run in.  If the system has an FVM, benchmarks run in
94/// a volume in the system FVM.  Otherwise, they run out of a partition in the system GPT (and might
95/// still include a hermetic FVM instance, if the benchmark needs it to run).
96pub enum BenchmarkVolumeFactory {
97    SystemFvm(
98        Box<dyn Send + Sync + Fn() -> fidl_fuchsia_fs_startup::VolumesProxy>,
99        Box<dyn Send + Sync + Fn() -> fio::DirectoryProxy>,
100    ),
101    SystemGpt(Arc<fpartitions::PartitionServiceProxy>),
102}
103
104struct RawBlockDeviceInGpt(Arc<fpartitions::PartitionServiceProxy>);
105
106impl BlockDevice for RawBlockDeviceInGpt {
107    fn connector(&self) -> Box<dyn BlockConnector> {
108        Box::new(self.0.clone())
109    }
110}
111
112#[async_trait]
113impl BlockDeviceFactory for BenchmarkVolumeFactory {
114    async fn create_block_device(&self, config: &BlockDeviceConfig) -> Box<dyn BlockDevice> {
115        let instance_guid = create_random_guid();
116        match self {
117            Self::SystemFvm(volumes_connector, _) => {
118                let volumes = volumes_connector();
119                Box::new(Self::create_fvm_volume(volumes, instance_guid, config).await)
120            }
121            Self::SystemGpt(partition_service) => {
122                if config.requires_fvm {
123                    Box::new(
124                        Self::create_fvm_instance_and_volume(
125                            partition_service.clone(),
126                            instance_guid,
127                            config,
128                        )
129                        .await,
130                    )
131                } else {
132                    Box::new(RawBlockDeviceInGpt(partition_service.clone()))
133                }
134            }
135        }
136    }
137}
138
139impl BenchmarkVolumeFactory {
140    /// Creates a factory for volumes in which benchmarks should run in based on the provided
141    /// configuration.  Uses various capabilities in the incoming namespace of the process.
142    pub async fn from_config(fxfs_blob: bool) -> BenchmarkVolumeFactory {
143        if fxfs_blob {
144            let partitions = Service::open(fpartitions::PartitionServiceMarker).unwrap();
145            let manager = connect_to_protocol::<fpartitions::PartitionsManagerMarker>().unwrap();
146            let instance =
147                BenchmarkVolumeFactory::connect_to_test_partition(partitions, manager).await;
148            assert!(
149                instance.is_some(),
150                "Failed to open or create testing FVM in GPT.  \
151                    Perhaps the system doesn't have a GPT-formatted block device?"
152            );
153            instance.unwrap()
154        } else {
155            let volumes_connector = Box::new(move || {
156                connect_to_protocol::<fidl_fuchsia_fs_startup::VolumesMarker>().unwrap()
157            });
158            let volumes_dir_connector = {
159                Box::new(move || {
160                    fuchsia_fs::directory::open_in_namespace("volumes", fio::PERM_READABLE).unwrap()
161                })
162            };
163            BenchmarkVolumeFactory::connect_to_system_fvm(volumes_connector, volumes_dir_connector)
164                .unwrap()
165        }
166    }
167
168    /// Connects to the system FVM component.
169    pub fn connect_to_system_fvm(
170        volumes_connector: Box<dyn Send + Sync + Fn() -> fidl_fuchsia_fs_startup::VolumesProxy>,
171        volumes_dir_connector: Box<dyn Send + Sync + Fn() -> fio::DirectoryProxy>,
172    ) -> Option<BenchmarkVolumeFactory> {
173        Some(BenchmarkVolumeFactory::SystemFvm(volumes_connector, volumes_dir_connector))
174    }
175
176    // Creates and connects to the partition reserved for benchmarks, or adds it to the GPT if
177    // absent.  The partition will be unformatted and should be reformatted explicitly before being
178    // used for a benchmark.
179    pub async fn connect_to_test_partition(
180        service: Service<fpartitions::PartitionServiceMarker>,
181        manager: fpartitions::PartitionsManagerProxy,
182    ) -> Option<BenchmarkVolumeFactory> {
183        let connector = block_matcher::find_or_create_test_partition(
184            service,
185            manager,
186            BENCHMARK_FVM_SIZE_BYTES,
187        )
188        .await
189        .expect("Failed to find or create test partition");
190
191        Some(BenchmarkVolumeFactory::SystemGpt(Arc::new(connector)))
192    }
193
194    #[cfg(test)]
195    pub async fn contains_fvm_volume(&self, name: &str) -> bool {
196        match self {
197            Self::SystemFvm(_, volumes_dir_connector) => {
198                let dir = volumes_dir_connector();
199                fuchsia_fs::directory::dir_contains(&dir, name).await.unwrap()
200            }
201            // If we're using a system GPT, the FVM instance is created on the fly, so volumes are
202            // too.
203            _ => false,
204        }
205    }
206
207    async fn create_fvm_volume(
208        volumes: fidl_fuchsia_fs_startup::VolumesProxy,
209        instance_guid: [u8; 16],
210        config: &BlockDeviceConfig,
211    ) -> FvmVolume {
212        let (volume_dir, crypt_task) = create_fvm_volume(&volumes, instance_guid, config).await;
213        let volumes = volumes.into_client_end().unwrap().into_sync_proxy();
214        FvmVolume {
215            destroy_fn: Some(Box::new(move || {
216                volumes
217                    .remove(BENCHMARK_VOLUME_NAME, zx::MonotonicInstant::INFINITE)
218                    .unwrap()
219                    .map_err(zx::Status::err_from_raw)
220            })),
221            volume_dir: Some(volume_dir),
222            fvm_instance: None,
223            block_path: format!("svc/{}", BlockMarker::PROTOCOL_NAME),
224            crypt_task,
225        }
226    }
227
228    async fn create_fvm_instance_and_volume(
229        partition: Arc<fpartitions::PartitionServiceProxy>,
230        instance_guid: [u8; 16],
231        config: &BlockDeviceConfig,
232    ) -> FvmVolume {
233        let mut fs = fs_management::filesystem::Filesystem::from_boxed_config(
234            Box::new(partition),
235            Box::new(Fvm { slice_size: BENCHMARK_FVM_SLICE_SIZE_BYTES, ..Fvm::default() }),
236        );
237        fs.format().await.expect("Failed to format FVM");
238        let fvm_instance = fs.serve_multi_volume().await.expect("Failed to serve FVM");
239        let volumes = connect_to_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumesMarker>(
240            fvm_instance.exposed_dir(),
241        )
242        .unwrap();
243
244        let (volume_dir, crypt_task) = create_fvm_volume(&volumes, instance_guid, config).await;
245        FvmVolume {
246            destroy_fn: None,
247            volume_dir: Some(volume_dir),
248            fvm_instance: Some(fvm_instance),
249            block_path: format!("svc/{}", BlockMarker::PROTOCOL_NAME),
250            crypt_task,
251        }
252    }
253}
254
255/// A block device created on top of an FVM instance.
256pub struct FvmVolume {
257    destroy_fn: Option<Box<dyn Send + Sync + FnOnce() -> Result<(), zx::Status>>>,
258    fvm_instance: Option<ServingMultiVolumeFilesystem>,
259    volume_dir: Option<fio::DirectoryProxy>,
260    crypt_task: Option<fasync::Task<()>>,
261    // The path in `volume_dir` to connect to when opening a new Block connection.
262    block_path: String,
263}
264
265impl BlockDevice for FvmVolume {
266    fn connector(&self) -> Box<dyn BlockConnector> {
267        let volume_dir = fuchsia_fs::directory::clone(self.volume_dir.as_ref().unwrap()).unwrap();
268        Box::new(DirBasedBlockConnector::new(volume_dir, self.block_path.clone()))
269    }
270}
271
272impl Drop for FvmVolume {
273    fn drop(&mut self) {
274        self.volume_dir = None;
275        self.fvm_instance = None;
276        self.crypt_task = None;
277        if let Some(destroy_fn) = self.destroy_fn.take() {
278            destroy_fn().expect("Failed to destroy FVM volume");
279        }
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use crate::testing::{RAMDISK_FVM_SLICE_SIZE, RamdiskFactory};
287    use block_client::RemoteBlockClient;
288    use fidl_fuchsia_fs_startup::VolumesMarker;
289    use fs_management::Gpt;
290    use ramdevice_client::{RamdiskClient, RamdiskClientBuilder};
291    use std::sync::Arc;
292    use vmo_backed_block_server::VmoBackedServer;
293
294    const BLOCK_SIZE: u64 = 4 * 1024;
295    const BLOCK_COUNT: u64 = 1024;
296    // We need more blocks for the GPT version of the test, since the library will by default
297    // allocate 128MiB for the embedded FVM.  This is big enough for a 192MiB device.
298    const GPT_BLOCK_COUNT: u64 = 49152;
299
300    #[fuchsia::test]
301    async fn ramdisk_create_block_device_with_zxcrypt() {
302        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
303        let _ = ramdisk_factory
304            .create_block_device(&BlockDeviceConfig {
305                requires_fvm: true,
306                use_zxcrypt: true,
307                volume_size: None,
308            })
309            .await;
310    }
311
312    #[fuchsia::test]
313    async fn ramdisk_create_block_device_without_zxcrypt() {
314        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
315        let _ = ramdisk_factory
316            .create_block_device(&BlockDeviceConfig {
317                requires_fvm: true,
318                use_zxcrypt: false,
319                volume_size: None,
320            })
321            .await;
322    }
323
324    #[fuchsia::test]
325    async fn ramdisk_create_block_device_without_volume_size() {
326        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
327        let ramdisk = ramdisk_factory
328            .create_block_device(&BlockDeviceConfig {
329                requires_fvm: true,
330                use_zxcrypt: false,
331                volume_size: None,
332            })
333            .await;
334        let volume_info = ramdisk
335            .connector()
336            .connect_block()
337            .unwrap()
338            .into_proxy()
339            .get_volume_info()
340            .await
341            .unwrap();
342        zx::ok(volume_info.0).unwrap();
343        let volume_info = volume_info.2.unwrap();
344        assert_eq!(volume_info.partition_slice_count, 1);
345    }
346
347    #[fuchsia::test]
348    async fn ramdisk_create_block_device_with_volume_size() {
349        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
350        let ramdisk = ramdisk_factory
351            .create_block_device(&BlockDeviceConfig {
352                requires_fvm: false,
353                use_zxcrypt: false,
354                volume_size: Some(RAMDISK_FVM_SLICE_SIZE as u64 * 3),
355            })
356            .await;
357        let volume_info = ramdisk
358            .connector()
359            .connect_block()
360            .unwrap()
361            .into_proxy()
362            .get_volume_info()
363            .await
364            .unwrap();
365        zx::ok(volume_info.0).unwrap();
366        let volume_info = volume_info.2.unwrap();
367        assert_eq!(volume_info.partition_slice_count, 3);
368    }
369
370    async fn init_gpt(block_size: u32, block_count: u64) -> zx::Vmo {
371        let vmo = zx::Vmo::create(block_size as u64 * block_count).unwrap();
372        let server = VmoBackedServer::from_vmo(
373            block_size,
374            vmo.create_child(zx::VmoChildOptions::REFERENCE, 0, 0).unwrap(),
375        )
376        .expect("Failed to create VmoBackedServer");
377        let (client, server_end) =
378            fidl::endpoints::create_proxy::<fidl_fuchsia_storage_block::BlockMarker>();
379
380        let _task =
381            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
382        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
383        gpt::Gpt::format(client.clone(), vec![gpt::PartitionInfo::nil(); 128])
384            .await
385            .expect("format failed");
386        vmo
387    }
388
389    struct FvmTestConfig {
390        fxblob_enabled: bool,
391    }
392
393    /// Retains test state.
394    ///
395    /// The ramdisk is running in an isolated devmgr instance, but the volume managers are
396    /// running in child components.
397    struct TestState(
398        #[allow(dead_code)] RamdiskClient,
399        #[allow(dead_code)] ServingMultiVolumeFilesystem,
400    );
401
402    async fn initialize(config: FvmTestConfig) -> (TestState, BenchmarkVolumeFactory) {
403        if config.fxblob_enabled {
404            // Initialize a new GPT.
405            let vmo = init_gpt(BLOCK_SIZE as u32, GPT_BLOCK_COUNT).await;
406            let ramdisk = RamdiskClientBuilder::new_with_vmo(vmo, Some(BLOCK_SIZE))
407                .build()
408                .await
409                .expect("Failed to create ramdisk");
410
411            let gpt = fs_management::filesystem::Filesystem::from_boxed_config(
412                ramdisk.connector().unwrap(),
413                Box::new(Gpt::dynamic_child()),
414            )
415            .serve_multi_volume()
416            .await
417            .expect("Failed to serve GPT");
418            let partitions =
419                Service::open_from_dir(gpt.exposed_dir(), fpartitions::PartitionServiceMarker)
420                    .unwrap();
421            let manager = connect_to_protocol_at_dir_root::<fpartitions::PartitionsManagerMarker>(
422                gpt.exposed_dir(),
423            )
424            .unwrap();
425            let fvm = BenchmarkVolumeFactory::connect_to_test_partition(partitions, manager)
426                .await
427                .expect("Failed to connect to FVM");
428            (TestState(ramdisk, gpt), fvm)
429        } else {
430            // Initialize a new FVM.
431            let ramdisk = RamdiskClientBuilder::new(BLOCK_SIZE, BLOCK_COUNT)
432                .build()
433                .await
434                .expect("Failed to create ramdisk");
435            let mut fs = fs_management::filesystem::Filesystem::from_boxed_config(
436                ramdisk.connector().unwrap(),
437                Box::new(Fvm { slice_size: RAMDISK_FVM_SLICE_SIZE, ..Fvm::dynamic_child() }),
438            );
439            fs.format().await.expect("Failed to format FVM");
440            let fvm_component = match fs.serve_multi_volume().await {
441                Ok(fvm_component) => fvm_component,
442                Err(_) => loop {},
443            };
444            let volumes_connector = {
445                let exposed_dir =
446                    fuchsia_fs::directory::clone(fvm_component.exposed_dir()).unwrap();
447                Box::new(move || {
448                    connect_to_protocol_at_dir_root::<VolumesMarker>(&exposed_dir).unwrap()
449                })
450            };
451            let volumes_dir_connector = {
452                let exposed_dir =
453                    fuchsia_fs::directory::clone(fvm_component.exposed_dir()).unwrap();
454                Box::new(move || {
455                    fuchsia_fs::directory::open_directory_async(
456                        &exposed_dir,
457                        "volumes",
458                        fio::PERM_READABLE,
459                    )
460                    .unwrap()
461                })
462            };
463            let fvm = BenchmarkVolumeFactory::connect_to_system_fvm(
464                volumes_connector,
465                volumes_dir_connector,
466            );
467            (TestState(ramdisk, fvm_component), fvm.unwrap())
468        }
469    }
470
471    async fn benchmark_volume_factory_can_find_fvm_instance(config: FvmTestConfig) {
472        let (_state, volume_factory) = initialize(config).await;
473
474        // Verify that a volume can be created.
475        volume_factory
476            .create_block_device(&BlockDeviceConfig {
477                requires_fvm: true,
478                use_zxcrypt: false,
479                volume_size: None,
480            })
481            .await;
482    }
483
484    #[fuchsia::test]
485    async fn benchmark_volume_factory_can_find_fvm_instance_fvm() {
486        benchmark_volume_factory_can_find_fvm_instance(FvmTestConfig { fxblob_enabled: false })
487            .await;
488    }
489
490    #[fuchsia::test]
491    async fn benchmark_volume_factory_can_find_fvm_instance_gpt() {
492        benchmark_volume_factory_can_find_fvm_instance(FvmTestConfig { fxblob_enabled: true })
493            .await;
494    }
495
496    async fn dropping_an_fvm_volume_removes_the_volume(config: FvmTestConfig) {
497        let (_state, volume_factory) = initialize(config).await;
498        {
499            let _volume = volume_factory
500                .create_block_device(&BlockDeviceConfig {
501                    requires_fvm: true,
502                    use_zxcrypt: false,
503                    volume_size: None,
504                })
505                .await;
506            assert!(volume_factory.contains_fvm_volume(BENCHMARK_VOLUME_NAME).await);
507        };
508        assert!(!volume_factory.contains_fvm_volume(BENCHMARK_VOLUME_NAME).await);
509    }
510
511    #[fuchsia::test]
512    async fn dropping_an_fvm_volume_removes_the_volume_fvm() {
513        dropping_an_fvm_volume_removes_the_volume(FvmTestConfig { fxblob_enabled: false }).await;
514    }
515
516    async fn benchmark_volume_factory_create_block_device_with_zxcrypt(config: FvmTestConfig) {
517        let (_state, volume_factory) = initialize(config).await;
518        let _ = volume_factory
519            .create_block_device(&BlockDeviceConfig {
520                requires_fvm: true,
521                use_zxcrypt: true,
522                volume_size: None,
523            })
524            .await;
525    }
526
527    #[fuchsia::test]
528    async fn benchmark_volume_factory_create_block_device_with_zxcrypt_fvm() {
529        benchmark_volume_factory_create_block_device_with_zxcrypt(FvmTestConfig {
530            fxblob_enabled: false,
531        })
532        .await;
533    }
534
535    #[fuchsia::test]
536    async fn benchmark_volume_factory_create_block_device_with_zxcrypt_gpt() {
537        benchmark_volume_factory_create_block_device_with_zxcrypt(FvmTestConfig {
538            fxblob_enabled: true,
539        })
540        .await;
541    }
542}