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_storage_block::BlockMarker;
12use fs_management::Fvm;
13use fs_management::filesystem::{
14    BlockConnector, DirBasedBlockConnector, ServingMultiVolumeFilesystem,
15};
16use fs_management::format::constants::{
17    BENCHMARK_FVM_TYPE_GUID, BENCHMARK_FVM_VOLUME_NAME, PAD_RW_PARTITION_LABEL,
18};
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};
23use storage_isolated_driver_manager::{
24    BlockDeviceMatcher, Guid, create_random_guid, find_block_device, fvm,
25};
26use {
27    fidl_fuchsia_io as fio, fidl_fuchsia_storage_partitions as fpartitions, fuchsia_async as fasync,
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: usize = 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 block_device =
284            partition.connect_block().expect("Failed to connect to block").into_proxy();
285        fvm::format_for_fvm(&block_device, BENCHMARK_FVM_SLICE_SIZE_BYTES)
286            .expect("Failed to format FVM");
287
288        let fs = fs_management::filesystem::Filesystem::from_boxed_config(
289            Box::new(partition),
290            Box::new(Fvm::default()),
291        );
292        let fvm_instance = fs.serve_multi_volume().await.expect("Failed to serve FVM");
293        let volumes = connect_to_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumesMarker>(
294            fvm_instance.exposed_dir(),
295        )
296        .unwrap();
297
298        let (volume_dir, crypt_task) = create_fvm_volume(&volumes, instance_guid, config).await;
299        FvmVolume {
300            destroy_fn: None,
301            volume_dir: Some(volume_dir),
302            fvm_instance: Some(fvm_instance),
303            block_path: format!("svc/{}", BlockMarker::PROTOCOL_NAME),
304            crypt_task,
305        }
306    }
307}
308
309/// A block device created on top of an FVM instance.
310pub struct FvmVolume {
311    destroy_fn: Option<Box<dyn Send + Sync + FnOnce() -> Result<(), zx::Status>>>,
312    fvm_instance: Option<ServingMultiVolumeFilesystem>,
313    volume_dir: Option<fio::DirectoryProxy>,
314    crypt_task: Option<fasync::Task<()>>,
315    // The path in `volume_dir` to connect to when opening a new Block connection.
316    block_path: String,
317}
318
319impl BlockDevice for FvmVolume {
320    fn connector(&self) -> Box<dyn BlockConnector> {
321        let volume_dir = fuchsia_fs::directory::clone(self.volume_dir.as_ref().unwrap()).unwrap();
322        Box::new(DirBasedBlockConnector::new(volume_dir, self.block_path.clone()))
323    }
324}
325
326impl Drop for FvmVolume {
327    fn drop(&mut self) {
328        self.volume_dir = None;
329        self.fvm_instance = None;
330        self.crypt_task = None;
331        if let Some(destroy_fn) = self.destroy_fn.take() {
332            destroy_fn().expect("Failed to destroy FVM volume");
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::testing::{RAMDISK_FVM_SLICE_SIZE, RamdiskFactory};
341    use block_client::RemoteBlockClient;
342    use fidl_fuchsia_fs_startup::VolumesMarker;
343    use fs_management::Gpt;
344    use ramdevice_client::{RamdiskClient, RamdiskClientBuilder};
345    use std::sync::Arc;
346    use vmo_backed_block_server::{VmoBackedServer, VmoBackedServerTestingExt as _};
347
348    const BLOCK_SIZE: u64 = 4 * 1024;
349    const BLOCK_COUNT: u64 = 1024;
350    // We need more blocks for the GPT version of the test, since the library will by default
351    // allocate 128MiB for the embedded FVM.  This is big enough for a 192MiB device.
352    const GPT_BLOCK_COUNT: u64 = 49152;
353
354    #[fuchsia::test]
355    async fn ramdisk_create_block_device_with_zxcrypt() {
356        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
357        let _ = ramdisk_factory
358            .create_block_device(&BlockDeviceConfig {
359                requires_fvm: true,
360                use_zxcrypt: true,
361                volume_size: None,
362            })
363            .await;
364    }
365
366    #[fuchsia::test]
367    async fn ramdisk_create_block_device_without_zxcrypt() {
368        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
369        let _ = ramdisk_factory
370            .create_block_device(&BlockDeviceConfig {
371                requires_fvm: true,
372                use_zxcrypt: false,
373                volume_size: None,
374            })
375            .await;
376    }
377
378    #[fuchsia::test]
379    async fn ramdisk_create_block_device_without_volume_size() {
380        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
381        let ramdisk = ramdisk_factory
382            .create_block_device(&BlockDeviceConfig {
383                requires_fvm: true,
384                use_zxcrypt: false,
385                volume_size: None,
386            })
387            .await;
388        let volume_info = ramdisk
389            .connector()
390            .connect_block()
391            .unwrap()
392            .into_proxy()
393            .get_volume_info()
394            .await
395            .unwrap();
396        zx::ok(volume_info.0).unwrap();
397        let volume_info = volume_info.2.unwrap();
398        assert_eq!(volume_info.partition_slice_count, 1);
399    }
400
401    #[fuchsia::test]
402    async fn ramdisk_create_block_device_with_volume_size() {
403        let ramdisk_factory = RamdiskFactory::new(BLOCK_SIZE, BLOCK_COUNT);
404        let ramdisk = ramdisk_factory
405            .create_block_device(&BlockDeviceConfig {
406                requires_fvm: false,
407                use_zxcrypt: false,
408                volume_size: Some(RAMDISK_FVM_SLICE_SIZE as u64 * 3),
409            })
410            .await;
411        let volume_info = ramdisk
412            .connector()
413            .connect_block()
414            .unwrap()
415            .into_proxy()
416            .get_volume_info()
417            .await
418            .unwrap();
419        zx::ok(volume_info.0).unwrap();
420        let volume_info = volume_info.2.unwrap();
421        assert_eq!(volume_info.partition_slice_count, 3);
422    }
423
424    async fn init_gpt(block_size: u32, block_count: u64) -> zx::Vmo {
425        let vmo = zx::Vmo::create(block_size as u64 * block_count).unwrap();
426        let server = Arc::new(VmoBackedServer::from_vmo(
427            block_size,
428            vmo.create_child(zx::VmoChildOptions::REFERENCE, 0, 0).unwrap(),
429        ));
430        let (client, server_end) =
431            fidl::endpoints::create_proxy::<fidl_fuchsia_storage_block::BlockMarker>();
432
433        let _task =
434            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
435        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
436        gpt::Gpt::format(client.clone(), vec![gpt::PartitionInfo::nil(); 128])
437            .await
438            .expect("format failed");
439        vmo
440    }
441
442    struct FvmTestConfig {
443        fxblob_enabled: bool,
444    }
445
446    /// Retains test state.
447    ///
448    /// The ramdisk is running in an isolated devmgr instance, but the volume managers are
449    /// running in child components.
450    struct TestState(
451        #[allow(dead_code)] RamdiskClient,
452        #[allow(dead_code)] ServingMultiVolumeFilesystem,
453    );
454
455    async fn initialize(config: FvmTestConfig) -> (TestState, BenchmarkVolumeFactory) {
456        if config.fxblob_enabled {
457            // Initialize a new GPT.
458            let vmo = init_gpt(BLOCK_SIZE as u32, GPT_BLOCK_COUNT).await;
459            let ramdisk = RamdiskClientBuilder::new_with_vmo(vmo, Some(BLOCK_SIZE))
460                .use_v2()
461                .build()
462                .await
463                .expect("Failed to create ramdisk");
464
465            let gpt = fs_management::filesystem::Filesystem::from_boxed_config(
466                ramdisk.connector().unwrap(),
467                Box::new(Gpt::dynamic_child()),
468            )
469            .serve_multi_volume()
470            .await
471            .expect("Failed to serve GPT");
472            let partitions =
473                Service::open_from_dir(gpt.exposed_dir(), fpartitions::PartitionServiceMarker)
474                    .unwrap();
475            let manager = connect_to_protocol_at_dir_root::<fpartitions::PartitionsManagerMarker>(
476                gpt.exposed_dir(),
477            )
478            .unwrap();
479            let fvm = BenchmarkVolumeFactory::connect_to_test_partition(partitions, manager)
480                .await
481                .expect("Failed to connect to FVM");
482            (TestState(ramdisk, gpt), fvm)
483        } else {
484            // Initialize a new FVM.
485            let ramdisk = RamdiskClientBuilder::new(BLOCK_SIZE, BLOCK_COUNT)
486                .use_v2()
487                .build()
488                .await
489                .expect("Failed to create ramdisk");
490            fvm::format_for_fvm(&ramdisk.open().unwrap().into_proxy(), RAMDISK_FVM_SLICE_SIZE)
491                .expect("Failed to format FVM");
492            let fvm_component = match fs_management::filesystem::Filesystem::from_boxed_config(
493                ramdisk.connector().unwrap(),
494                Box::new(Fvm::dynamic_child()),
495            )
496            .serve_multi_volume()
497            .await
498            {
499                Ok(fvm_component) => fvm_component,
500                Err(_) => loop {},
501            };
502            let volumes_connector = {
503                let exposed_dir =
504                    fuchsia_fs::directory::clone(fvm_component.exposed_dir()).unwrap();
505                Box::new(move || {
506                    connect_to_protocol_at_dir_root::<VolumesMarker>(&exposed_dir).unwrap()
507                })
508            };
509            let volumes_dir_connector = {
510                let exposed_dir =
511                    fuchsia_fs::directory::clone(fvm_component.exposed_dir()).unwrap();
512                Box::new(move || {
513                    fuchsia_fs::directory::open_directory_async(
514                        &exposed_dir,
515                        "volumes",
516                        fio::PERM_READABLE,
517                    )
518                    .unwrap()
519                })
520            };
521            let fvm = BenchmarkVolumeFactory::connect_to_system_fvm(
522                volumes_connector,
523                volumes_dir_connector,
524            );
525            (TestState(ramdisk, fvm_component), fvm.unwrap())
526        }
527    }
528
529    async fn benchmark_volume_factory_can_find_fvm_instance(config: FvmTestConfig) {
530        let (_state, volume_factory) = initialize(config).await;
531
532        // Verify that a volume can be created.
533        volume_factory
534            .create_block_device(&BlockDeviceConfig {
535                requires_fvm: true,
536                use_zxcrypt: false,
537                volume_size: None,
538            })
539            .await;
540    }
541
542    #[fuchsia::test]
543    async fn benchmark_volume_factory_can_find_fvm_instance_fvm() {
544        benchmark_volume_factory_can_find_fvm_instance(FvmTestConfig { fxblob_enabled: false })
545            .await;
546    }
547
548    #[fuchsia::test]
549    async fn benchmark_volume_factory_can_find_fvm_instance_gpt() {
550        benchmark_volume_factory_can_find_fvm_instance(FvmTestConfig { fxblob_enabled: true })
551            .await;
552    }
553
554    async fn dropping_an_fvm_volume_removes_the_volume(config: FvmTestConfig) {
555        let (_state, volume_factory) = initialize(config).await;
556        {
557            let _volume = volume_factory
558                .create_block_device(&BlockDeviceConfig {
559                    requires_fvm: true,
560                    use_zxcrypt: false,
561                    volume_size: None,
562                })
563                .await;
564            assert!(volume_factory.contains_fvm_volume(BENCHMARK_VOLUME_NAME).await);
565        };
566        assert!(!volume_factory.contains_fvm_volume(BENCHMARK_VOLUME_NAME).await);
567    }
568
569    #[fuchsia::test]
570    async fn dropping_an_fvm_volume_removes_the_volume_fvm() {
571        dropping_an_fvm_volume_removes_the_volume(FvmTestConfig { fxblob_enabled: false }).await;
572    }
573
574    async fn benchmark_volume_factory_create_block_device_with_zxcrypt(config: FvmTestConfig) {
575        let (_state, volume_factory) = initialize(config).await;
576        let _ = volume_factory
577            .create_block_device(&BlockDeviceConfig {
578                requires_fvm: true,
579                use_zxcrypt: true,
580                volume_size: None,
581            })
582            .await;
583    }
584
585    #[fuchsia::test]
586    async fn benchmark_volume_factory_create_block_device_with_zxcrypt_fvm() {
587        benchmark_volume_factory_create_block_device_with_zxcrypt(FvmTestConfig {
588            fxblob_enabled: false,
589        })
590        .await;
591    }
592
593    #[fuchsia::test]
594    async fn benchmark_volume_factory_create_block_device_with_zxcrypt_gpt() {
595        benchmark_volume_factory_create_block_device_with_zxcrypt(FvmTestConfig {
596            fxblob_enabled: true,
597        })
598        .await;
599    }
600}