wyyerd / stripe-rs

Rust API bindings for the Stripe HTTP API.
Apache License 2.0
225 stars 88 forks source link

Binary size #96

Open ThouCheese opened 5 years ago

ThouCheese commented 5 years ago

The current binary size of stripe-rs is quite dramatic. I was investigating why the RAM usage of one of my services had increased by 5 MB per instance and I noticed that the binary size of my service had increased dramatically. I ran cargo-bloat to determine the origin of this increase, and I found that of the final 18 MB (after stripping) used by the binary, 5.6 MB was used by Stripe. The output is as follows:

[thoucheese@craptop file_size]$ cargo bloat --crates -n 10 --release
Compiling ...
Analyzing target/release/file_size
 File  .text     Size Crate
20.6%  37.0%   5.0MiB stripe
16.1%  28.8%   3.9MiB file_size
 4.3%   7.7%   1.1MiB std
 2.4%   4.2% 589.2KiB reqwest
 1.2%   2.1% 291.8KiB rocket
 1.1%   2.0% 283.9KiB handlebars
 0.9%   1.7% 230.7KiB regex_syntax
 0.9%   1.5% 214.0KiB regex
 0.8%   1.4% 198.5KiB serde
 0.7%   1.2% 165.2KiB hyper
 6.8%  12.2%   1.7MiB And 90 more crates. Use -n N to show more.
55.7% 100.0%  13.6MiB .text section size, the file size is 24.4MiB

Further investigation (just running cargo bloat, without the --crates), shows that stripe-rust generates some very large deserialize implementations for the large enums and structs it uses:

[luuk@craptop file_size]$ cargo bloat -n 10 --release
Compiling ...
Analyzing target/release/file_size
 File  .text    Size                 Crate Name
 0.3%   0.5% 76.1KiB                stripe <stripe::resources::issuing_merchant_data::_IMPL_DESERIALIZE_FOR_MerchantCategory::<impl serde::de::Deserialize for stripe::resources::issuing_merchant_data::MerchantCategory>::deserialize::__...
 0.3%   0.5% 76.1KiB                 file_size <stripe::resources::issuing_merchant_data::_IMPL_DESERIALIZE_FOR_MerchantCategory::<impl serde::de::Deserialize for stripe::resources::issuing_merchant_data::MerchantCategory>::deserialize::__...
 0.2%   0.4% 54.9KiB                 regex <regex::exec::ExecNoSync as regex::re_trait::RegularExpression>::captures_read_at
 0.2%   0.3% 41.5KiB               stripe? <stripe::resources::invoice::_IMPL_DESERIALIZE_FOR_Invoice::<impl serde::de::Deserialize for stripe::resources::invoice::Invoice>::deserialize::__Visitor as serde::de::Visitor>::visit_map
 0.2%   0.3% 38.9KiB               stripe? <stripe::resources::charge::_IMPL_DESERIALIZE_FOR_Charge::<impl serde::de::Deserialize for stripe::resources::charge::Charge>::deserialize::__Visitor as serde::de::Visitor>::visit_map
 0.2%   0.3% 38.9KiB               stripe? <stripe::resources::charge::_IMPL_DESERIALIZE_FOR_Charge::<impl serde::de::Deserialize for stripe::resources::charge::Charge>::deserialize::__Visitor as serde::de::Visitor>::visit_map
 0.1%   0.3% 36.8KiB unicode_normalization unicode_normalization::tables::compatibility_fully_decomposed
 0.1%   0.3% 36.8KiB unicode_normalization unicode_normalization::tables::compatibility_fully_decomposed
 0.1%   0.3% 36.6KiB               stripe? <stripe::resources::charge::_IMPL_DESERIALIZE_FOR_Charge::<impl serde::de::Deserialize for stripe::resources::charge::Charge>::deserialize::__Visitor as serde::de::Visitor>::visit_map
 0.1%   0.2% 34.7KiB               stripe? <stripe::resources::charge::_IMPL_DESERIALIZE_FOR_Charge::<impl serde::de::Deserialize for stripe::resources::charge::Charge>::deserialize::__Visitor as serde::de::Visitor>::visit_map
53.8%  96.6% 13.1MiB                       And 25202 smaller methods. Use -n N to show more.
55.7% 100.0% 13.6MiB                       .text section size, the file size is 24.4MiB

Is this a known issue? And is there any interest in reducing this size? And if so, how can I contribute to this?

ThouCheese commented 5 years ago

Possibly related: My 2500 LOC project takes 1 minute and 20 seconds for a release build. When I exclude the module that includes stripe-rust, that time drops to 15 seconds. Could it be that the code generation and the monomorphization is putting a lot of stress on the compiler? Fox example, the generic functions in client.rs are all monomorphized for each type, maybe the speed could be improved upon by using dynamic dispatch?

kestred commented 5 years ago

Avoiding monomorphization via dynamic dispatch seems like a good idea. I think that would be a good change to make.

I'm not quite sure how to address the size of Serialize/Deserialize impls. One approach is that most folks probably only use a subset of the stripe API; we could use compiler feature flags to avoid compiling features that aren't used, maybe including just core and (maybe payment methods?) by default.

It's worth seeing how much of the compiled size that would cut out.

ThouCheese commented 4 years ago

On the one hand the API is it is right now is easy and adding "using that part requires this feature flag" will reduce that. However, I think that the trade-off in terms of compile time is definitely worth it.

Also, as is visible in the second snippet in the issue, the serde impls seem to be generated twice, once for the current crate, and once for the stripe crate. I don't know if this is a fundamental restriction with how the binary is generated, or that its is a bug with serde, or that something is not configured optimally within stripe-rs.

kestred commented 4 years ago

@ThouCheese: A 50-60% improvement to binary size / bloat can be had as of tag:v0.12.0-alpha.1, with default-features = false and just enabling the APIs that you use.

The code on provides #100 another ~%10 decrease to binary size.

There are also some improvements to compile time; partially from reduced upstream dependencies and partially from disabling unused features. I haven't specifically focused on improving compile time yet.

Next, I'm hoping to determine whether the builtin #[derive(Deserialize)]) is generating impls with unnecessary bloat.

ThouCheese commented 3 years ago

Hi @kestred, impressive results! I am only now revisiting this issue because I am investigating compilation times of another project. The remarkable thing is that even with default features turned off, stripe-rust still takes up the majority of the compilation time. I am sure that investigating the serde::Serialize and serde::Deserialize implementations is a good idea, because for example a program like this:

#[derive(serde::Deserialize, serde::Serialize)]
pub struct Discount {
    // Always true for a deleted object
    #[serde(default)]
    pub deleted: bool,
    /// The subscription that this coupon is applied to, if it is applied to a particular subscription.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subscription: Option<String>,
}

fn main() {}

Already expands into over 300 lines of code after these macros are done. In fact in this issue, which is old but still open, dtolnay asks for people that have problems with the binary size / compilation time of serde-using applications.