Fun with Rust

Coding Oct 5, 2021

Lately i've been playing around with a programming language called Rust, learning and poking around at various crates and language features. I'm really intregued with Rust because I began my interest in software development with more low level languages and always had an interest in going deeper. I'm also really interested in the fact that you can compile Rust programs for a number of platforms and that it's typically extremely fast.

The Project

As a way to see what the performance is like vs what is likely worst case scenario I decided to take a peice of code that we use at Otis AI and rewrite it in Rust to compare run times. This particular peice of code takes about 108 minutes to run with Javascript on my local machine. Since this code is an internal tool I cannot share the source or too many details about it's function however I can share some basic information. The overall idea of this function is to

  1. Pull a ton of records from MongoDB ~5000
  2. Pull data from different web services for each record
  3. Calculate some information from each record
  4. Make a request to another web service for each record

The overall idea is fairly simple but the code is pretty complex and in desperate need of a good rewrite. While I could never pass off Rust as an alternative to our current stack this is just for fun 😉

Rust Implementation

With an outline of what was required I knew I needed some crates to use with this program. One for the database and one for the HTTP requests to web services. I did some looking and eventually settled on reqwest for the HTTP requests, the official mongodb crate for the database query and serde for serializing various data into and out of structs.

I also have been experimenting and learning about different ways to structure code in Rust so for this project I decided to use a structure with each service I was using in it's own directory in a mod.rs file so each service is like so service/mod.rs then in main.rs I simply pull in the module with mod service; each module contains relevant struct definitions as well which are declared public so we can use them in other files.

Setting them up I tested the query which returned all 5000+ records in about 11 seconds, which seemed slow. So I optimized the query to only pull specific fields which provided a fairly large performance increase. Satisfied with the results I decided to write the HTTP API calls with reqwest which turned out to be pretty straight forward. I did realize though that some of these requests required a ton of url parameters so I decided to go against my better judgement and add another crate url which lets you easily add url parameters.

Now my requests looked something like this:

#[derive(Serialize, Deserialize)]
pub struct ExampleResponse {
  pub data: Option<String>
}

pub async fn get_something(account: &str, token: &str) -> Result<ExampleResponse, Box<dyn Error>> {
  let mut url = 
    Url::parse(format!("https://some.api/path/{}", account).as_str()).unwrap();

  url.query_pairs_mut().append_pair("stuff", "things");
  url.query_pairs_mut().append_pair("token", token);

  let uri = url.as_str();

  let resp = reqwest::get(uri)
    .await?
    .json::<ExampleResponse>()
    .await?;

  Ok(resp)
}

Note the distinct lack of error handling, because i'm still not sure how to do that yet 😅

Now I copied this with slight modifications to create the rest of the requests that I needed for this experiment and started putting together the main.rs which will simply run the program in it's entirety and time it. The next issue that I ran into was that I would need a lot of configuration keys for this program that I wouldn't want to have to recompile in order to change. To resolve this I searched for a crate that would let me have an external configuration file similar to dot-env in node I ended up finding the config crate which fit my needs exactly and set it up with the variables that I needed, then loaded them like this

let mut settings = config::Config::default();
settings.merge(config::File::with_name("Config")).unwrap();

let database = settings.get_str("database").unwrap();
//...

Optimizing

Running the program like this I was able to achieve some really great performance results but I wanted to add some threading since this is the perfect kind of task for that and it would likely benefit significantly. I really wanted to create some sort of thread pool to allow processing of the items in chunks of a defined number that way it would be more contained but provide great performance benefit. I also wanted to be able to configure the number of threads with the configuraton file so that I could refine it for various machines and test out what worked best. To do this I found a cool blog post describing just what I needed and reworked it to fit the program. My implementation differs slightly and looks something like this

let threads: usize = settings.get_int("threads").unwrap_or(250).try_into().unwrap();

let fetches = futures::stream::iter(items.into_iter().map(|item| async {
  // do work..
  
})).buffer_unordered(threads).collect::<Vec<()>>();

Then I decided to clean up the code a bit and use struct to pass arguments into functions since many calls were being made with the same arguments. So I modified each function to take a struct with the proper variables and for some reason I am still not sure of this one change seemed to increase performance by about 40 seconds with a release build! That's wild.

Results

With my program now optimized as much as I know how I ran a release build with timing and here are the results!

With Rust I was able to make this particular function run 540 times faster than the Javascript implementation! That's a massive benefit in performance.

Final Thoughts

Obviously the Rust program would need a lot more code to make it a production software like proper error handling, but for a challenge and experiment this was a pretty cool project. I'm really excited about the results and for future learning of the Rust language.

Tags

Cryptobyte

Senior Software Developer working with software since age 17 (all the way back in 2008!)

Comments

Sign in or become a Cryptobyte member to join the conversation.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.