Skip to main content

aws_smithy_eventstream/
message_size_hint.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Provides size hint functionality for Event Stream messages to optimize buffer allocation.
7
8use aws_smithy_types::event_stream::{HeaderValue, Message};
9use std::mem::size_of;
10
11/// Extension trait that provides size hint functionality for Event Stream messages.
12///
13/// This trait allows callers to get an accurate estimate of the serialized size
14/// of a message before serialization, enabling optimal buffer pre-allocation.
15pub trait MessageSizeHint {
16    /// Returns an estimate of the serialized size of this message in bytes.
17    ///
18    /// This provides a hint for buffer allocation to improve performance when
19    /// serializing the message. The estimate includes the message prelude,
20    /// headers, payload, and CRC checksums.
21    ///
22    /// # Examples
23    ///
24    /// ```
25    /// use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
26    /// use aws_smithy_eventstream::message_size_hint::MessageSizeHint;
27    /// use bytes::Bytes;
28    ///
29    /// let message = Message::new(Bytes::from(b"hello world".to_vec()))
30    ///     .add_header(Header::new("content-type", HeaderValue::String("text/plain".into())));
31    ///
32    /// let size_hint = message.size_hint();
33    /// // Use the size hint to pre-allocate a buffer
34    /// let mut buffer: Vec<u8> = Vec::with_capacity(size_hint);
35    /// ```
36    fn size_hint(&self) -> usize;
37}
38
39impl MessageSizeHint for Message {
40    fn size_hint(&self) -> usize {
41        // Constants from the frame format
42        const PRELUDE_LENGTH_BYTES: usize = 3 * size_of::<u32>();
43        const MESSAGE_CRC_LENGTH_BYTES: usize = size_of::<u32>();
44        const MAX_HEADER_NAME_LEN: usize = 255;
45
46        // Calculate headers size
47        let mut headers_len = 0;
48        for header in self.headers() {
49            let name_len = header.name().as_bytes().len().min(MAX_HEADER_NAME_LEN);
50            headers_len += 1 + name_len; // name length byte + name bytes
51
52            // Add header value size based on type
53            headers_len += match header.value() {
54                HeaderValue::Bool(_) => 1,                            // type byte only
55                HeaderValue::Byte(_) => 2,                            // type + value
56                HeaderValue::Int16(_) => 3,                           // type + value
57                HeaderValue::Int32(_) => 5,                           // type + value
58                HeaderValue::Int64(_) => 9,                           // type + value
59                HeaderValue::ByteArray(val) => 3 + val.len(),         // type + length + data
60                HeaderValue::String(val) => 3 + val.as_bytes().len(), // type + length + data
61                HeaderValue::Timestamp(_) => 9,                       // type + value
62                HeaderValue::Uuid(_) => 17,                           // type + value
63                _ => 0, // Handle any future header value types conservatively
64            };
65        }
66
67        let payload_len = self.payload().len();
68
69        // Total message size: prelude + headers + payload + message CRC
70        PRELUDE_LENGTH_BYTES + headers_len + payload_len + MESSAGE_CRC_LENGTH_BYTES
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::frame::write_message_to;
78    use aws_smithy_types::event_stream::{Header, HeaderValue, Message};
79    use aws_smithy_types::DateTime;
80    use bytes::Bytes;
81
82    #[test]
83    fn test_size_hint_accuracy() {
84        // Test cases with different message configurations
85        let test_cases = vec![
86            // Simple message with small payload
87            Message::new(Bytes::from(b"hello world".to_vec())),
88            // Message with headers
89            Message::new(Bytes::from(b"test payload".to_vec())).add_header(Header::new(
90                "content-type",
91                HeaderValue::String("application/json".into()),
92            )),
93            // Message with multiple headers and different value types
94            Message::new(Bytes::from(b"complex test".to_vec()))
95                .add_header(Header::new("bool-true", HeaderValue::Bool(true)))
96                .add_header(Header::new("bool-false", HeaderValue::Bool(false)))
97                .add_header(Header::new("byte-val", HeaderValue::Byte(42)))
98                .add_header(Header::new("int16-val", HeaderValue::Int16(12345)))
99                .add_header(Header::new("int32-val", HeaderValue::Int32(987654321)))
100                .add_header(Header::new(
101                    "int64-val",
102                    HeaderValue::Int64(1234567890123456789),
103                ))
104                .add_header(Header::new(
105                    "string-val",
106                    HeaderValue::String("hello world".into()),
107                ))
108                .add_header(Header::new(
109                    "bytes-val",
110                    HeaderValue::ByteArray(Bytes::from(b"binary data".to_vec())),
111                ))
112                .add_header(Header::new(
113                    "timestamp-val",
114                    HeaderValue::Timestamp(DateTime::from_secs(5_000_000)),
115                ))
116                .add_header(Header::new("uuid-val", HeaderValue::Uuid(u128::MAX))),
117            // Empty payload
118            Message::new(Bytes::new()),
119            // Large payload (1KB)
120            Message::new(Bytes::from(vec![0u8; 1024])).add_header(Header::new(
121                "large-content",
122                HeaderValue::String("large payload test".into()),
123            )),
124        ];
125
126        for (i, message) in test_cases.iter().enumerate() {
127            let size_hint = message.size_hint();
128
129            // Get actual serialized size
130            let mut buffer = Vec::new();
131            write_message_to(message, &mut buffer)
132                .unwrap_or_else(|_| panic!("Failed to serialize test case {i}"));
133            let actual_size = buffer.len();
134
135            // The size hint should exactly match the actual serialized size
136            assert_eq!(
137                size_hint, actual_size,
138                "Size hint mismatch for test case {i}: hint={size_hint}, actual={actual_size}"
139            );
140        }
141    }
142
143    #[test]
144    fn test_size_hint_with_long_header_name() {
145        // Test with a header name that's near the maximum length
146        let long_name = "x".repeat(200); // Long but valid header name
147        let message = Message::new(Bytes::from(b"payload".to_vec())).add_header(Header::new(
148            long_name,
149            HeaderValue::String("long header name test".into()),
150        ));
151
152        let size_hint = message.size_hint();
153
154        let mut buffer = Vec::new();
155        write_message_to(&message, &mut buffer)
156            .expect("Failed to serialize message with long header name");
157        let actual_size = buffer.len();
158
159        assert_eq!(
160            size_hint, actual_size,
161            "Size hint should match actual size for long header names"
162        );
163    }
164
165    #[test]
166    fn test_size_hint_performance_benefit() {
167        // Create a message with 1KB payload
168        let message = Message::new(Bytes::from(vec![0u8; 1024])).add_header(Header::new(
169            "content-type",
170            HeaderValue::String("application/json".into()),
171        ));
172
173        let size_hint = message.size_hint();
174
175        // Verify that using size hint for pre-allocation works
176        let mut buffer = Vec::with_capacity(size_hint);
177        write_message_to(&message, &mut buffer).expect("Failed to serialize message");
178
179        // The buffer should not have needed to reallocate
180        assert!(
181            buffer.capacity() >= buffer.len(),
182            "Buffer should have sufficient capacity"
183        );
184
185        // The size hint should be reasonably close to actual size
186        assert_eq!(
187            size_hint,
188            buffer.len(),
189            "Size hint should exactly match serialized size"
190        );
191    }
192}