22
33use std:: {
44 error:: Error as StdError ,
5- future:: Future ,
65 io:: { self , Write as _} ,
76 pin:: Pin ,
87 task:: { Context , Poll } ,
98} ;
109
11- use actix_rt:: task:: { spawn_blocking, JoinHandle } ;
12- use bytes:: Bytes ;
10+ use bytes:: { Buf , Bytes } ;
1311use derive_more:: Display ;
1412#[ cfg( feature = "compress-gzip" ) ]
1513use flate2:: write:: { GzEncoder , ZlibEncoder } ;
@@ -26,14 +24,12 @@ use crate::{
2624 ResponseHead , StatusCode ,
2725} ;
2826
29- const MAX_CHUNK_SIZE_ENCODE_IN_PLACE : usize = 1024 ;
30-
3127pin_project ! {
3228 pub struct Encoder <B > {
3329 #[ pin]
3430 body: EncoderBody <B >,
35- encoder: Option <ContentEncoder >,
36- fut : Option <JoinHandle < Result < ContentEncoder , io :: Error >> >,
31+ encoder: Option <SelectedContentEncoder >,
32+ chunk_ready_to_encode : Option <Bytes >,
3733 eof: bool ,
3834 }
3935}
@@ -45,7 +41,7 @@ impl<B: MessageBody> Encoder<B> {
4541 body : body:: None :: new ( ) ,
4642 } ,
4743 encoder : None ,
48- fut : None ,
44+ chunk_ready_to_encode : None ,
4945 eof : true ,
5046 }
5147 }
@@ -68,13 +64,13 @@ impl<B: MessageBody> Encoder<B> {
6864
6965 if should_encode {
7066 // wrap body only if encoder is feature-enabled
71- if let Some ( enc ) = ContentEncoder :: select ( encoding) {
67+ if let Some ( selected_encoder ) = ContentEncoder :: select ( encoding) {
7268 update_head ( encoding, head) ;
7369
7470 return Encoder {
7571 body,
76- encoder : Some ( enc ) ,
77- fut : None ,
72+ encoder : Some ( selected_encoder ) ,
73+ chunk_ready_to_encode : None ,
7874 eof : false ,
7975 } ;
8076 }
@@ -83,10 +79,19 @@ impl<B: MessageBody> Encoder<B> {
8379 Encoder {
8480 body,
8581 encoder : None ,
86- fut : None ,
82+ chunk_ready_to_encode : None ,
8783 eof : false ,
8884 }
8985 }
86+
87+ pub fn with_encode_chunk_size ( mut self , size : usize ) -> Self {
88+ if size > 0 {
89+ if let Some ( selected_encoder) = self . encoder . as_mut ( ) {
90+ selected_encoder. preferred_chunk_size = size;
91+ }
92+ }
93+ self
94+ }
9095}
9196
9297pin_project ! {
@@ -169,22 +174,30 @@ where
169174 return Poll :: Ready ( None ) ;
170175 }
171176
172- if let Some ( ref mut fut ) = this. fut {
173- let mut encoder = ready ! ( Pin :: new ( fut ) . poll ( cx ) )
174- . map_err ( |_| {
175- EncoderError :: Io ( io :: Error :: new (
176- io :: ErrorKind :: Other ,
177- "Blocking task was cancelled unexpectedly" ,
178- ) )
179- } ) ?
177+ if let Some ( chunk ) = this. chunk_ready_to_encode . as_mut ( ) {
178+ let selected_encoder = this . encoder . as_mut ( ) . expect (
179+ "when chunk_ready_to_encode is presented the encoder is expected to be presented as well" ,
180+ ) ;
181+ let encode_len = chunk . len ( ) . min ( selected_encoder . preferred_chunk_size ) ;
182+ selected_encoder
183+ . content_encoder
184+ . write ( & chunk [ ..encode_len ] )
180185 . map_err ( EncoderError :: Io ) ?;
186+ chunk. advance ( encode_len) ;
181187
182- let chunk = encoder. take ( ) ;
183- * this. encoder = Some ( encoder) ;
184- this. fut . take ( ) ;
188+ if chunk. is_empty ( ) {
189+ * this. chunk_ready_to_encode = None ;
190+ }
191+
192+ let encoded_chunk = selected_encoder. content_encoder . take ( ) ;
193+ if !encoded_chunk. is_empty ( ) {
194+ return Poll :: Ready ( Some ( Ok ( encoded_chunk) ) ) ;
195+ }
185196
186- if !chunk. is_empty ( ) {
187- return Poll :: Ready ( Some ( Ok ( chunk) ) ) ;
197+ if this. chunk_ready_to_encode . is_some ( ) {
198+ // Yield execution to give chance other futures to execute
199+ cx. waker ( ) . wake_by_ref ( ) ;
200+ return Poll :: Pending ;
188201 }
189202 }
190203
@@ -194,29 +207,18 @@ where
194207 Some ( Err ( err) ) => return Poll :: Ready ( Some ( Err ( err) ) ) ,
195208
196209 Some ( Ok ( chunk) ) => {
197- if let Some ( mut encoder) = this. encoder . take ( ) {
198- if chunk. len ( ) < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
199- encoder. write ( & chunk) . map_err ( EncoderError :: Io ) ?;
200- let chunk = encoder. take ( ) ;
201- * this. encoder = Some ( encoder) ;
202-
203- if !chunk. is_empty ( ) {
204- return Poll :: Ready ( Some ( Ok ( chunk) ) ) ;
205- }
206- } else {
207- * this. fut = Some ( spawn_blocking ( move || {
208- encoder. write ( & chunk) ?;
209- Ok ( encoder)
210- } ) ) ;
211- }
212- } else {
210+ if this. encoder . is_none ( ) {
213211 return Poll :: Ready ( Some ( Ok ( chunk) ) ) ;
214212 }
213+ * this. chunk_ready_to_encode = Some ( chunk) ;
215214 }
216215
217216 None => {
218- if let Some ( encoder) = this. encoder . take ( ) {
219- let chunk = encoder. finish ( ) . map_err ( EncoderError :: Io ) ?;
217+ if let Some ( selected_encoder) = this. encoder . take ( ) {
218+ let chunk = selected_encoder
219+ . content_encoder
220+ . finish ( )
221+ . map_err ( EncoderError :: Io ) ?;
220222
221223 if chunk. is_empty ( ) {
222224 return Poll :: Ready ( None ) ;
@@ -276,28 +278,56 @@ enum ContentEncoder {
276278 Zstd ( ZstdEncoder < ' static , Writer > ) ,
277279}
278280
281+ struct SelectedContentEncoder {
282+ content_encoder : ContentEncoder ,
283+ preferred_chunk_size : usize ,
284+ }
285+
279286impl ContentEncoder {
280- fn select ( encoding : ContentEncoding ) -> Option < Self > {
287+ fn select ( encoding : ContentEncoding ) -> Option < SelectedContentEncoder > {
288+ // Chunk size picked as max chunk size which took less that 50 µs to compress on "cargo bench --bench compression-chunk-size"
289+
290+ // Rust 1.72 linux/arm64 in Docker on Apple M2 Pro: "time to compress chunk/deflate-16384" time: [39.114 µs 39.283 µs 39.457 µs]
291+ const MAX_DEFLATE_CHUNK_SIZE : usize = 16384 ;
292+ // Rust 1.72 linux/arm64 in Docker on Apple M2 Pro: "time to compress chunk/gzip-16384" time: [40.121 µs 40.340 µs 40.566 µs]
293+ const MAX_GZIP_CHUNK_SIZE : usize = 16384 ;
294+ // Rust 1.72 linux/arm64 in Docker on Apple M2 Pro: "time to compress chunk/br-8192" time: [46.076 µs 46.208 µs 46.343 µs]
295+ const MAX_BROTLI_CHUNK_SIZE : usize = 8192 ;
296+ // Rust 1.72 linux/arm64 in Docker on Apple M2 Pro: "time to compress chunk/zstd-16384" time: [32.872 µs 32.967 µs 33.068 µs]
297+ const MAX_ZSTD_CHUNK_SIZE : usize = 16384 ;
298+
281299 match encoding {
282300 #[ cfg( feature = "compress-gzip" ) ]
283- ContentEncoding :: Deflate => Some ( ContentEncoder :: Deflate ( ZlibEncoder :: new (
284- Writer :: new ( ) ,
285- flate2:: Compression :: fast ( ) ,
286- ) ) ) ,
301+ ContentEncoding :: Deflate => Some ( SelectedContentEncoder {
302+ content_encoder : ContentEncoder :: Deflate ( ZlibEncoder :: new (
303+ Writer :: new ( ) ,
304+ flate2:: Compression :: fast ( ) ,
305+ ) ) ,
306+ preferred_chunk_size : MAX_DEFLATE_CHUNK_SIZE ,
307+ } ) ,
287308
288309 #[ cfg( feature = "compress-gzip" ) ]
289- ContentEncoding :: Gzip => Some ( ContentEncoder :: Gzip ( GzEncoder :: new (
290- Writer :: new ( ) ,
291- flate2:: Compression :: fast ( ) ,
292- ) ) ) ,
310+ ContentEncoding :: Gzip => Some ( SelectedContentEncoder {
311+ content_encoder : ContentEncoder :: Gzip ( GzEncoder :: new (
312+ Writer :: new ( ) ,
313+ flate2:: Compression :: fast ( ) ,
314+ ) ) ,
315+ preferred_chunk_size : MAX_GZIP_CHUNK_SIZE ,
316+ } ) ,
293317
294318 #[ cfg( feature = "compress-brotli" ) ]
295- ContentEncoding :: Brotli => Some ( ContentEncoder :: Brotli ( new_brotli_compressor ( ) ) ) ,
319+ ContentEncoding :: Brotli => Some ( SelectedContentEncoder {
320+ content_encoder : ContentEncoder :: Brotli ( new_brotli_compressor ( ) ) ,
321+ preferred_chunk_size : MAX_BROTLI_CHUNK_SIZE ,
322+ } ) ,
296323
297324 #[ cfg( feature = "compress-zstd" ) ]
298325 ContentEncoding :: Zstd => {
299326 let encoder = ZstdEncoder :: new ( Writer :: new ( ) , 3 ) . ok ( ) ?;
300- Some ( ContentEncoder :: Zstd ( encoder) )
327+ Some ( SelectedContentEncoder {
328+ content_encoder : ContentEncoder :: Zstd ( encoder) ,
329+ preferred_chunk_size : MAX_ZSTD_CHUNK_SIZE ,
330+ } )
301331 }
302332
303333 _ => None ,
@@ -426,3 +456,77 @@ impl From<EncoderError> for crate::Error {
426456 crate :: Error :: new_encoder ( ) . with_cause ( err)
427457 }
428458}
459+
460+ #[ cfg( test) ]
461+ mod tests {
462+ use bytes:: BytesMut ;
463+ use rand:: { seq:: SliceRandom , Rng } ;
464+
465+ use super :: * ;
466+
467+ static EMPTY_BODY : & [ u8 ] = & [ ] ;
468+
469+ static SHORT_BODY : & [ u8 ] = & [ 1 , 2 , 3 , 4 , 6 , 7 , 8 ] ;
470+
471+ static LONG_BODY : & [ u8 ] = include_bytes ! ( "encoder.rs" ) ;
472+
473+ static BODIES : & [ & [ u8 ] ] = & [ EMPTY_BODY , SHORT_BODY , LONG_BODY ] ;
474+
475+ async fn test_compression_of_conentent_enconding ( encoding : ContentEncoding , body : & [ u8 ] ) {
476+ let mut head = ResponseHead :: new ( StatusCode :: OK ) ;
477+ let body_to_compress = {
478+ let mut body = BytesMut :: from ( body) ;
479+ body. shuffle ( & mut rand:: thread_rng ( ) ) ;
480+ body. freeze ( )
481+ } ;
482+ let compressed_body = Encoder :: response ( encoding, & mut head, body_to_compress. clone ( ) )
483+ . with_encode_chunk_size ( rand:: thread_rng ( ) . gen_range ( 32 ..128 ) ) ;
484+
485+ let SelectedContentEncoder {
486+ content_encoder : mut compressor,
487+ preferred_chunk_size : _,
488+ } = ContentEncoder :: select ( encoding) . unwrap ( ) ;
489+ compressor. write ( & body_to_compress) . unwrap ( ) ;
490+ let reference_compressed_bytes = compressor. finish ( ) . unwrap ( ) ;
491+
492+ let compressed_bytes =
493+ body:: to_bytes_limited ( compressed_body, 256 + body_to_compress. len ( ) )
494+ . await
495+ . unwrap ( )
496+ . unwrap ( ) ;
497+
498+ assert_eq ! ( reference_compressed_bytes, compressed_bytes) ;
499+ }
500+
501+ #[ actix_rt:: test]
502+ #[ cfg( feature = "compress-gzip" ) ]
503+ async fn test_gzip_compression_in_chunks_is_the_same_as_whole_chunk_compression ( ) {
504+ for body in BODIES {
505+ test_compression_of_conentent_enconding ( ContentEncoding :: Gzip , body) . await ;
506+ }
507+ }
508+
509+ #[ actix_rt:: test]
510+ #[ cfg( feature = "compress-gzip" ) ]
511+ async fn test_deflate_compression_in_chunks_is_the_same_as_whole_chunk_compression ( ) {
512+ for body in BODIES {
513+ test_compression_of_conentent_enconding ( ContentEncoding :: Deflate , body) . await ;
514+ }
515+ }
516+
517+ #[ actix_rt:: test]
518+ #[ cfg( feature = "compress-brotli" ) ]
519+ async fn test_brotli_compression_in_chunks_is_the_same_as_whole_chunk_compression ( ) {
520+ for body in BODIES {
521+ test_compression_of_conentent_enconding ( ContentEncoding :: Brotli , body) . await ;
522+ }
523+ }
524+
525+ #[ actix_rt:: test]
526+ #[ cfg( feature = "compress-zstd" ) ]
527+ async fn test_zstd_compression_in_chunks_is_the_same_as_whole_chunk_compression ( ) {
528+ for body in BODIES {
529+ test_compression_of_conentent_enconding ( ContentEncoding :: Zstd , body) . await ;
530+ }
531+ }
532+ }
0 commit comments