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