Skip to content

Instantly share code, notes, and snippets.

@bsodmike
Last active April 3, 2024 12:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bsodmike/d5aa13e7262e84c79d967562bc2cadd0 to your computer and use it in GitHub Desktop.
Save bsodmike/d5aa13e7262e84c79d967562bc2cadd0 to your computer and use it in GitHub Desktop.
Towards Impeccable Rust by Jon Gjengset, session at Rust Nation UK (27th March 2024)

Towards Impeccable Rust

Testing

  • Miri
  • Test for error conditions

Embrace chaos (fuzzing)

  • Async/sync chaos Turmoil / shuttle
  • Value checks: quickcheck / proptest
  • Logic chaos: cargo-mutants

Be Exhaustive

  • Loom - all possible and distinguishable concurrent executions
  • Kani: all possible and distinguishable inputs (symbolic execution) - use for concurrent access, unsafe code, where bugs are likely

Benchmarks

  • Know thy self: Capture the entire performance profile of your program. Not just the easy common path.
    • Pathological cases:
    • Micro and macro: individual don’t get slower, and you want end to end tests.
    • Under, at and over capacity: if the system is not loaded, you don’t want to be using X (too many) CPU cores; at capacity, you can tolerate that load. With “overload” of input, the system doesn’t failover.
    • On all relevant targets

Are your benchmarks useful?

Have trustworthy measurements

Let your CI fail on regression. This isn’t easy!

  • Don’t use time (incl. ops/second) / Iai-callgrind
  • Run old and new interleaved / tango
  • Minimise noise / dedicated host + <100% load

Speed isn’t all

Measure all that matters

  • Throughput and “Goodput”
  • Memory use (avg. and max.)
  • Latency Outcomes:
  • Simulate real-life inputs
  • Measure system outputs
  • Compare output to ground truth

Simple benchmarks lie

  • How you benchmark matters: open vs closed vs partly-open respresentative workloads
  • What you record matters: mean, median, histogram, cdc
  • How you compare matters, y > x is not sufficien

Documentation

Document decisions taken

  • What alternatives were discarded and why?
  • What downsides were explicitly accepted and why? No protract method, but make sure there’s something. Try (Y)ARDs?

Document what’s not there, what shortcuts did we take?

  • Missing handles of corder-cases: todo!() / unimplemented!()
  • Future optimisation opportunities
  • Absence of an impl (like From)

Misuse resistance

You’re holding it wrong is not acceptable in these settings

  • NewType: so no one can confuse Meters(u64) with Miles(u64)
    • struct Meters(u64)
  • Typestates
    • Rocket<S> with an example Rocket<Ground>
    • rocket.launch(/* … */) -> Rocket<Air>
  • Two-phase structs: TomlConfig vs ResolvedConfig (??)
  • Enums over booleans
  • Enums for linked arguments

Follow idioms

Surprise and misuse go hand in hand

  • Clippy is your friend
  • The Rust API Guidelines
  • If it smells like OOP or Python/Java/C think again - ahh that smell…
    • Use Builders, not “Factories”
    • Use mutable references to self, not free functions (newbie alert!!)
    • Arc everywhere
    • Traits with 3+ super traits, trying to “mock” inheritance in a bad way.

Compatibility

Stagnation the root of all evil. There are more of these than you think.

Minimise hazards

  • Concrete types
    • Prefer -> impl Trait
    • Avoid pub fields
    • Use generics for arguments or impl Traits for return positions to allow flexibility for future code changes / You’ll be locked into a specific type (such as Vec or HashMap) and to change this you’ll have to break backwards compatibility. Make the API you promise the user is non restrictive on your future code changes as much as possible.
  • Public dependencies
    • Args, return types, trait implies etc.
    • If you take a public dependency and these leak out into the public API of your crate, say module re-exports, return types etc, such as hyper v0.14 what this means is you cannot upgrade to hyper 1 without breaking your consumers.
  • impl From
    • Prefer non-pub inherent methods!
    • Trait implementations are always public. If I implement From serde_yaml::Value then serde_yaml can never be removed from my dependencies ( a breaking change0

Humans miss things

Dependencies

Knowledge is everything

Healthy skepticism is a given.

  • Then tracking everything: Complete dependency closure and every deployment and device
  • Then connecting to known issues, i.e join against RUSTSEC
  • Then vetting for unknown issues: cargo-vet public or internal

Make stagnation a recurrent choice

  • Loud reminders when:
    • You’re behind
    • Dependencies are dead
  • Reduce friction to catching up:
    • Auto-merging bump PRs
    • Budget for maintenance work
    • Upstream changes (no forks!)
    • Wrap unstable dependencies.
  • Applies to rustc version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment