diff --git a/fuzz/fuzz_targets/file_io.rs b/fuzz/fuzz_targets/file_io.rs index eea63039fc6..2c306561723 100644 --- a/fuzz/fuzz_targets/file_io.rs +++ b/fuzz/fuzz_targets/file_io.rs @@ -17,6 +17,7 @@ use vortex_array::dtype::StructFields; use vortex_array::expr::lit; use vortex_array::expr::root; use vortex_array::scalar_fn::fns::operators::Operator; +use vortex_btrblocks::BtrBlocksCompressorBuilder; use vortex_buffer::ByteBufferMut; use vortex_error::VortexExpect; use vortex_error::vortex_panic; @@ -59,12 +60,11 @@ fuzz_target!(|fuzz: FuzzFileAction| -> Corpus { let write_options = match compressor_strategy { CompressorStrategy::Default => SESSION.write_options(), - CompressorStrategy::Compact => { - let strategy = WriteStrategyBuilder::default() - .with_compact_encodings() - .build(); - SESSION.write_options().with_strategy(strategy) - } + CompressorStrategy::Compact => SESSION.write_options().with_strategy( + WriteStrategyBuilder::default() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()) + .build(), + ), }; let mut full_buff = ByteBufferMut::empty(); diff --git a/vortex-bench/src/lib.rs b/vortex-bench/src/lib.rs index 6dad0f0f6a1..a1b7c46bb16 100644 --- a/vortex-bench/src/lib.rs +++ b/vortex-bench/src/lib.rs @@ -26,6 +26,7 @@ use tpcds::TpcDsBenchmark; use tpch::benchmark::TpcHBenchmark; pub use utils::file::*; pub use utils::logging::*; +use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::error::VortexExpect; use vortex::error::vortex_err; use vortex::file::VortexWriteOptions; @@ -231,7 +232,7 @@ impl CompactionStrategy { match self { CompactionStrategy::Compact => options.with_strategy( WriteStrategyBuilder::default() - .with_compact_encodings() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()) .build(), ), CompactionStrategy::Default => options, diff --git a/vortex-btrblocks/public-api.lock b/vortex-btrblocks/public-api.lock index b2336456828..4c5dbcb1e77 100644 --- a/vortex-btrblocks/public-api.lock +++ b/vortex-btrblocks/public-api.lock @@ -614,7 +614,9 @@ impl vortex_btrblocks::BtrBlocksCompressorBuilder pub fn vortex_btrblocks::BtrBlocksCompressorBuilder::build(self) -> vortex_btrblocks::BtrBlocksCompressor -pub fn vortex_btrblocks::BtrBlocksCompressorBuilder::exclude(self, ids: impl core::iter::traits::collect::IntoIterator) -> Self +pub fn vortex_btrblocks::BtrBlocksCompressorBuilder::exclude_schemes(self, ids: impl core::iter::traits::collect::IntoIterator) -> Self + +pub fn vortex_btrblocks::BtrBlocksCompressorBuilder::only_cuda_compatible(self) -> Self pub fn vortex_btrblocks::BtrBlocksCompressorBuilder::with_compact(self) -> Self diff --git a/vortex-btrblocks/src/builder.rs b/vortex-btrblocks/src/builder.rs index 3b6531a3941..08650bf6185 100644 --- a/vortex-btrblocks/src/builder.rs +++ b/vortex-btrblocks/src/builder.rs @@ -67,7 +67,7 @@ pub const ALL_SCHEMES: &[&dyn Scheme] = &[ /// /// By default, all schemes in [`ALL_SCHEMES`] are enabled. Feature-gated schemes (Pco, Zstd) /// are not in `ALL_SCHEMES` and must be added explicitly via -/// [`with_new_scheme`](BtrBlocksCompressorBuilder::with_new_scheme) or +/// [`with_scheme`](BtrBlocksCompressorBuilder::with_new_scheme) or /// [`with_compact`](BtrBlocksCompressorBuilder::with_compact). /// /// # Examples @@ -79,9 +79,9 @@ pub const ALL_SCHEMES: &[&dyn Scheme] = &[ /// // Default compressor with all schemes in ALL_SCHEMES. /// let compressor = BtrBlocksCompressorBuilder::default().build(); /// -/// // Exclude specific schemes. +/// // Remove specific schemes. /// let compressor = BtrBlocksCompressorBuilder::default() -/// .exclude([IntDictScheme.id()]) +/// .exclude_schemes([IntDictScheme.id()]) /// .build(); /// ``` #[derive(Debug, Clone)] @@ -100,8 +100,8 @@ impl Default for BtrBlocksCompressorBuilder { impl BtrBlocksCompressorBuilder { /// Adds an external compression scheme not in [`ALL_SCHEMES`]. /// - /// This allows encoding crates outside of `vortex-btrblocks` to register their own schemes with - /// the compressor. + /// This allows encoding crates outside of `vortex-btrblocks` to register their own schemes + /// with the compressor. /// /// # Panics /// @@ -128,7 +128,6 @@ impl BtrBlocksCompressorBuilder { /// Panics if any of the compact schemes are already present. #[cfg(feature = "zstd")] pub fn with_compact(self) -> Self { - // This should be fast since we don't have that many schemes. let builder = self.with_new_scheme(&string::ZstdScheme); #[cfg(feature = "pco")] @@ -139,8 +138,32 @@ impl BtrBlocksCompressorBuilder { builder } - /// Excludes the specified compression schemes by their [`SchemeId`]. - pub fn exclude(mut self, ids: impl IntoIterator) -> Self { + /// Excludes schemes without CUDA kernel support and adds Zstd for string compression. + /// + /// With the `unstable_encodings` feature, buffer-level Zstd compression is used which + /// preserves the array buffer layout for zero-conversion GPU decompression. Without it, + /// interleaved Zstd compression is used. + #[cfg(feature = "zstd")] + pub fn only_cuda_compatible(self) -> Self { + let builder = self.exclude_schemes([ + integer::SparseScheme.id(), + rle::RLE_INTEGER_SCHEME.id(), + rle::RLE_FLOAT_SCHEME.id(), + float::NullDominatedSparseScheme.id(), + string::StringDictScheme.id(), + string::FSSTScheme.id(), + ]); + + #[cfg(feature = "unstable_encodings")] + let builder = builder.with_new_scheme(&string::ZstdBuffersScheme); + #[cfg(not(feature = "unstable_encodings"))] + let builder = builder.with_new_scheme(&string::ZstdScheme); + + builder + } + + /// Removes the specified compression schemes by their [`SchemeId`]. + pub fn exclude_schemes(mut self, ids: impl IntoIterator) -> Self { let ids: HashSet<_> = ids.into_iter().collect(); self.schemes.retain(|s| !ids.contains(&s.id())); self diff --git a/vortex-btrblocks/src/canonical_compressor.rs b/vortex-btrblocks/src/canonical_compressor.rs index 19e737d6060..885178dd22d 100644 --- a/vortex-btrblocks/src/canonical_compressor.rs +++ b/vortex-btrblocks/src/canonical_compressor.rs @@ -25,9 +25,9 @@ use crate::CascadingCompressor; /// // Default compressor - all schemes allowed. /// let compressor = BtrBlocksCompressor::default(); /// -/// // Exclude specific schemes using the builder. +/// // Remove specific schemes using the builder. /// let compressor = BtrBlocksCompressorBuilder::default() -/// .exclude([IntDictScheme.id()]) +/// .exclude_schemes([IntDictScheme.id()]) /// .build(); /// ``` #[derive(Clone)] diff --git a/vortex-btrblocks/src/lib.rs b/vortex-btrblocks/src/lib.rs index 5ea55a40c66..a17429de56d 100644 --- a/vortex-btrblocks/src/lib.rs +++ b/vortex-btrblocks/src/lib.rs @@ -46,9 +46,9 @@ //! // Default compressor with all schemes enabled. //! let compressor = BtrBlocksCompressor::default(); //! -//! // Configure with builder to exclude specific schemes. +//! // Remove specific schemes using the builder. //! let compressor = BtrBlocksCompressorBuilder::default() -//! .exclude([IntDictScheme.id()]) +//! .exclude_schemes([IntDictScheme.id()]) //! .build(); //! ``` //! diff --git a/vortex-cuda/gpu-scan-cli/src/main.rs b/vortex-cuda/gpu-scan-cli/src/main.rs index 72b907d896b..fac73e69263 100644 --- a/vortex-cuda/gpu-scan-cli/src/main.rs +++ b/vortex-cuda/gpu-scan-cli/src/main.rs @@ -21,6 +21,7 @@ use vortex::VortexSessionDefault; use vortex::array::ToCanonical; use vortex::array::arrays::Dict; use vortex::buffer::ByteBufferMut; +use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::error::VortexResult; use vortex::file::OpenOptionsSessionExt; use vortex::file::WriteOptionsSessionExt; @@ -92,7 +93,7 @@ async fn main() -> VortexResult<()> { #[cuda_available] fn cuda_write_strategy() -> Arc { WriteStrategyBuilder::default() - .with_cuda_compatible_encodings() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().only_cuda_compatible()) .with_flat_strategy(Arc::new(CudaFlatLayoutStrategy::default())) .build() } diff --git a/vortex-file/public-api.lock b/vortex-file/public-api.lock index 69dddbbdb88..4d896437a3b 100644 --- a/vortex-file/public-api.lock +++ b/vortex-file/public-api.lock @@ -346,12 +346,10 @@ pub fn vortex_file::WriteStrategyBuilder::build(self) -> alloc::sync::Arc Self -pub fn vortex_file::WriteStrategyBuilder::with_compact_encodings(self) -> Self +pub fn vortex_file::WriteStrategyBuilder::with_btrblocks_builder(self, builder: vortex_btrblocks::builder::BtrBlocksCompressorBuilder) -> Self pub fn vortex_file::WriteStrategyBuilder::with_compressor(self, compressor: C) -> Self -pub fn vortex_file::WriteStrategyBuilder::with_cuda_compatible_encodings(self) -> Self - pub fn vortex_file::WriteStrategyBuilder::with_field_writer(self, field: impl core::convert::Into, writer: alloc::sync::Arc) -> Self pub fn vortex_file::WriteStrategyBuilder::with_flat_strategy(self, flat: alloc::sync::Arc) -> Self diff --git a/vortex-file/src/strategy.rs b/vortex-file/src/strategy.rs index c855a825c17..197efd9583f 100644 --- a/vortex-file/src/strategy.rs +++ b/vortex-file/src/strategy.rs @@ -28,7 +28,6 @@ use vortex_array::arrays::VarBinView; use vortex_array::dtype::FieldPath; use vortex_array::session::ArrayRegistry; use vortex_array::session::ArraySession; -use vortex_btrblocks::BtrBlocksCompressor; use vortex_btrblocks::BtrBlocksCompressorBuilder; use vortex_btrblocks::SchemeExt; use vortex_btrblocks::schemes::integer::IntDictScheme; @@ -59,14 +58,6 @@ use vortex_sequence::Sequence; use vortex_sparse::Sparse; use vortex_utils::aliases::hash_map::HashMap; use vortex_zigzag::ZigZag; - -#[rustfmt::skip] -#[cfg(feature = "zstd")] -use vortex_btrblocks::{ - schemes::float, - schemes::integer, - schemes::string, -}; #[cfg(feature = "zstd")] use vortex_zstd::Zstd; #[cfg(all(feature = "zstd", feature = "unstable_encodings"))] @@ -123,13 +114,24 @@ pub static ALLOWED_ENCODINGS: LazyLock = LazyLock::new(|| { session.registry().clone() }); -/// Build a new [writer strategy][LayoutStrategy] to compress and reorganize chunks of a Vortex file. +/// How the compressor was configured on [`WriteStrategyBuilder`]. +enum CompressorConfig { + /// A [`BtrBlocksCompressorBuilder`] that [`WriteStrategyBuilder::build`] will finalize. + /// `IntDictScheme` is automatically excluded from the data compressor to prevent recursive + /// dictionary encoding. + BtrBlocks(BtrBlocksCompressorBuilder), + /// An opaque compressor used as-is for both data and stats compression. + Opaque(Arc), +} + +/// Build a new [writer strategy](LayoutStrategy) to compress and reorganize chunks of a Vortex +/// file. /// /// Vortex provides an out-of-the-box file writer that optimizes the layout of chunks on-disk, /// repartitioning and compressing them to strike a balance between size on-disk, /// bulk decoding performance, and IOPS required to perform an indexed read. pub struct WriteStrategyBuilder { - compressor_override: Option>, + compressor: CompressorConfig, row_block_size: usize, field_writers: HashMap>, allow_encodings: Option, @@ -141,7 +143,7 @@ impl Default for WriteStrategyBuilder { /// and then finally built yielding the [`LayoutStrategy`]. fn default() -> Self { Self { - compressor_override: None, + compressor: CompressorConfig::BtrBlocks(BtrBlocksCompressorBuilder::default()), row_block_size: 8192, field_writers: HashMap::new(), allow_encodings: Some(ALLOWED_ENCODINGS.clone()), @@ -183,97 +185,20 @@ impl WriteStrategyBuilder { self } - /// Override the [compressor](CompressorPlugin) used for compressing chunks in the file. - /// - /// If not provided, this will use a BtrBlocks-style cascading compressor that tries to balance - /// total size with decoding performance. - /// - /// # Panics - /// - /// Panics if a compressor has already been set via - /// [`with_compressor`](Self::with_compressor), - /// [`with_cuda_compatible_encodings`](Self::with_cuda_compatible_encodings), or - /// [`with_compact_encodings`](Self::with_compact_encodings). - /// - /// These methods are mutually exclusive. - pub fn with_compressor(mut self, compressor: C) -> Self { - assert!( - self.compressor_override.is_none(), - "A compressor has already been configured. `with_compressor`, \ - `with_cuda_compatible_encodings`, and `with_compact_encodings` are mutually exclusive." - ); - self.compressor_override = Some(Arc::new(compressor)); - self - } - - /// Configure a write strategy that emits only CUDA-compatible encodings. - /// - /// This method simply exists as a wrapper around [`with_compressor`]. - /// - /// This configures BtrBlocks to exclude schemes without CUDA kernel support. - /// With the `unstable_encodings` feature, strings use buffer-level Zstd compression - /// (`ZstdBuffersArray`) which preserves the array buffer layout for zero-conversion - /// GPU decompression. Without it, strings use interleaved Zstd compression. - /// - /// # Panics - /// - /// Panics if a compressor has already been set. See [`with_compressor`] + /// Override the default [`BtrBlocksCompressorBuilder`] used for compression. /// - /// [`with_compressor`]: Self::with_compressor. - #[cfg(feature = "zstd")] - pub fn with_cuda_compatible_encodings(mut self) -> Self { - assert!( - self.compressor_override.is_none(), - "A compressor has already been configured. `with_compressor`, \ - `with_cuda_compatible_encodings`, and `with_compact_encodings` are mutually exclusive." - ); - - let mut builder = BtrBlocksCompressorBuilder::default().exclude([ - integer::SparseScheme.id(), - integer::RLE_INTEGER_SCHEME.id(), - float::RLE_FLOAT_SCHEME.id(), - float::NullDominatedSparseScheme.id(), - string::StringDictScheme.id(), - string::FSSTScheme.id(), - ]); - - #[cfg(feature = "unstable_encodings")] - { - builder = builder.with_new_scheme(&string::ZstdBuffersScheme); - } - #[cfg(not(feature = "unstable_encodings"))] - { - builder = builder.with_new_scheme(&string::ZstdScheme); - } - - self.compressor_override = Some(Arc::new(builder.build())); + /// The builder is finalized during [`build`](Self::build), producing two compressors: one for + /// data (with `IntDictScheme` excluded) and one for stats. + pub fn with_btrblocks_builder(mut self, builder: BtrBlocksCompressorBuilder) -> Self { + self.compressor = CompressorConfig::BtrBlocks(builder); self } - /// Configure a write strategy that uses compact encodings (Pco for numerics, Zstd for - /// strings/binary). - /// - /// This method simply exists as a wrapper around [`with_compressor`]. - /// - /// This provides better compression ratios than the default BtrBlocks strategy, - /// especially for floating-point heavy datasets. + /// Set the compressor to an opaque [`CompressorPlugin`]. /// - /// # Panics - /// - /// Panics if a compressor has already been set. See [`with_compressor`] - /// - /// [`with_compressor`]: Self::with_compressor. - #[cfg(feature = "zstd")] - pub fn with_compact_encodings(mut self) -> Self { - assert!( - self.compressor_override.is_none(), - "A compressor has already been configured. `with_compressor`, \ - `with_cuda_compatible_encodings`, and `with_compact_encodings` are mutually exclusive." - ); - - self.compressor_override = Some(Arc::new( - BtrBlocksCompressorBuilder::default().with_compact().build(), - )); + /// The compressor is used as-is for both data and stats compression. + pub fn with_compressor(mut self, compressor: C) -> Self { + self.compressor = CompressorConfig::Opaque(Arc::new(compressor)); self } @@ -294,19 +219,18 @@ impl WriteStrategyBuilder { let buffered = BufferedStrategy::new(chunked, 2 * ONE_MEG); // 2MB // 5. compress each chunk. - // Exclude IntDictScheme from the default compressor because DictStrategy (step 3) already + // Exclude IntDictScheme from the data compressor because DictStrategy (step 3) already // dictionary-encodes columns. Allowing IntDictScheme here would redundantly // dictionary-encode the integer codes produced by that earlier step. - let data_compressor: Arc = - if let Some(ref compressor) = self.compressor_override { - compressor.clone() - } else { - Arc::new( - BtrBlocksCompressorBuilder::default() - .exclude([IntDictScheme.id()]) - .build(), - ) - }; + let data_compressor: Arc = match &self.compressor { + CompressorConfig::BtrBlocks(builder) => Arc::new( + builder + .clone() + .exclude_schemes([IntDictScheme.id()]) + .build(), + ), + CompressorConfig::Opaque(compressor) => compressor.clone(), + }; let compressing = CompressingStrategy::new(buffered, data_compressor); // 4. prior to compression, coalesce up to a minimum size @@ -327,12 +251,10 @@ impl WriteStrategyBuilder { ); // 2.1. | 3.1. compress stats tables and dict values. - let stats_compressor: Arc = - if let Some(ref compressor) = self.compressor_override { - compressor.clone() - } else { - Arc::new(BtrBlocksCompressor::default()) - }; + let stats_compressor: Arc = match self.compressor { + CompressorConfig::BtrBlocks(builder) => Arc::new(builder.build()), + CompressorConfig::Opaque(compressor) => compressor, + }; let compress_then_flat = CompressingStrategy::new(flat, stats_compressor); // 3. apply dict encoding or fallback diff --git a/vortex-python/src/io.rs b/vortex-python/src/io.rs index bf60a4b2d5e..edf1061188d 100644 --- a/vortex-python/src/io.rs +++ b/vortex-python/src/io.rs @@ -15,6 +15,7 @@ use vortex::array::arrow::FromArrowArray; use vortex::array::iter::ArrayIterator; use vortex::array::iter::ArrayIteratorAdapter; use vortex::array::iter::ArrayIteratorExt; +use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::dtype::DType; use vortex::dtype::arrow::FromArrowType; use vortex::error::VortexError; @@ -352,7 +353,8 @@ impl PyVortexWriteOptions { py.detach(|| { let mut strategy = WriteStrategyBuilder::default(); if self.use_compact_encodings { - strategy = strategy.with_compact_encodings(); + strategy = strategy + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()); } let strategy = strategy.build(); TOKIO_RUNTIME.block_on(async move { diff --git a/vortex-test/compat-gen/src/fixtures/arrays/datasets/mod.rs b/vortex-test/compat-gen/src/fixtures/arrays/datasets/mod.rs index 8a727f0b8f8..c799bc7a380 100644 --- a/vortex-test/compat-gen/src/fixtures/arrays/datasets/mod.rs +++ b/vortex-test/compat-gen/src/fixtures/arrays/datasets/mod.rs @@ -17,6 +17,7 @@ pub fn fixtures() -> Vec> { #[cfg(test)] mod tests { + use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::file::WriteStrategyBuilder; use super::fixtures; @@ -43,7 +44,7 @@ mod tests { let compact_bytes = adapter::write_compressed_to_bytes( array, WriteStrategyBuilder::default() - .with_compact_encodings() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()) .build(), ) .unwrap(); diff --git a/vortex-test/compat-gen/src/fixtures/mod.rs b/vortex-test/compat-gen/src/fixtures/mod.rs index 65c1893bfb9..edf0dd47e37 100644 --- a/vortex-test/compat-gen/src/fixtures/mod.rs +++ b/vortex-test/compat-gen/src/fixtures/mod.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use vortex::array::ArrayId; use vortex::array::ArrayRef; +use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::file::WriteStrategyBuilder; use vortex_error::VortexResult; use vortex_error::vortex_bail; @@ -135,7 +136,7 @@ impl Fixture for DatasetFixtureAdapter { let path = dir.join(self.name()); if self.compact { let strategy = WriteStrategyBuilder::default() - .with_compact_encodings() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()) .build(); adapter::write_compressed(&path, array, strategy)?; } else { diff --git a/vortex-tui/src/convert.rs b/vortex-tui/src/convert.rs index 240ed34d8fa..f6798413422 100644 --- a/vortex-tui/src/convert.rs +++ b/vortex-tui/src/convert.rs @@ -15,6 +15,7 @@ use tokio::io::AsyncWriteExt; use vortex::array::ArrayRef; use vortex::array::arrow::FromArrowArray; use vortex::array::stream::ArrayStreamAdapter; +use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::dtype::DType; use vortex::dtype::arrow::FromArrowType; use vortex::error::VortexExpect; @@ -91,11 +92,11 @@ pub async fn exec_convert(session: &VortexSession, flags: ConvertArgs) -> anyhow .boxed(); } - let strategy = WriteStrategyBuilder::default(); - let strategy = match flags.strategy { - Strategy::Btrblocks => strategy, - Strategy::Compact => strategy.with_compact_encodings(), - }; + let mut strategy = WriteStrategyBuilder::default(); + if matches!(flags.strategy, Strategy::Compact) { + strategy = + strategy.with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()); + } let mut file = File::create(output_path).await?; session diff --git a/vortex/examples/tracing_vortex.rs b/vortex/examples/tracing_vortex.rs index b8a8cdefefa..ab4920e0f89 100644 --- a/vortex/examples/tracing_vortex.rs +++ b/vortex/examples/tracing_vortex.rs @@ -39,6 +39,7 @@ use vortex::array::arrays::StructArray; use vortex::array::arrays::VarBinArray; use vortex::array::stream::ArrayStreamExt; use vortex::array::validity::Validity; +use vortex::compressor::BtrBlocksCompressorBuilder; use vortex::dtype::DType; use vortex::dtype::Nullability; use vortex::file::WriteStrategyBuilder; @@ -388,7 +389,7 @@ async fn write_batch_to_vortex( // Use compact encodings (Pco + Zstd) for the telemetry files. let write_opts = session.write_options().with_strategy( WriteStrategyBuilder::default() - .with_compact_encodings() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()) .build(), ); diff --git a/vortex/src/lib.rs b/vortex/src/lib.rs index 6d6199263a4..c84055d92aa 100644 --- a/vortex/src/lib.rs +++ b/vortex/src/lib.rs @@ -195,6 +195,7 @@ mod test { use vortex_array::expr::select; use vortex_array::stream::ArrayStreamExt; use vortex_array::validity::Validity; + use vortex_btrblocks::BtrBlocksCompressorBuilder; use vortex_buffer::buffer; use vortex_error::VortexResult; use vortex_file::OpenOptionsSessionExt; @@ -309,7 +310,7 @@ mod test { .write_options() .with_strategy( WriteStrategyBuilder::default() - .with_compact_encodings() + .with_btrblocks_builder(BtrBlocksCompressorBuilder::default().with_compact()) .build(), ) .write(