diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..e9f2f13 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [ecton] \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..edd43e9 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: Docs + +on: [push, pull_request] + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: nightly + + - uses: actions/checkout@v3 + - name: Generate Docs + run: | + cargo +nightly doc --no-deps --all-features + + - name: Deploy Docs + if: github.ref == 'refs/heads/main' + uses: JamesIves/github-pages-deploy-action@releases/v4 + with: + branch: gh-pages + folder: target/doc/ + git-config-name: kl-botsu + git-config-email: botsu@khonsulabs.com + target-folder: /main/ + clean: true diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..dae6ed2 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,51 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + + - name: Run clippy + run: | + cargo clippy + + - name: Run unit tests + run: | + cargo test --all-targets + + miri: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + with: + components: cargo,rustc,rust-std,miri + rust-version: nightly + + - name: Run unit tests + run: | + cargo +nightly miri test + env: + MIRIFLAGS: "-Zmiri-permissive-provenance -Zmiri-ignore-leaks" + + build-msrv: + name: Test on MSRV + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: 1.65 + - name: Run unit tests + run: cargo test --all-targets diff --git a/Cargo.lock b/Cargo.lock index f4d6168..21c7169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,6 +287,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "intentional" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc48117ac1523428c576e39831e93043112e2d2be0223bb0c0593af944e4e38" + [[package]] name = "is-terminal" version = "0.4.12" @@ -374,6 +380,7 @@ dependencies = [ "criterion", "crossbeam-utils", "flume 0.11.0", + "intentional", "kempt", "nanorand", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index bbfeefb..554dde3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,13 @@ name = "musegc" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +rust-version = "1.73.0" [dependencies] ahash = "0.8.11" crossbeam-utils = "0.8.19" flume = "0.11.0" +intentional = "0.1.1" kempt = "0.2.3" nanorand = { version = "0.7.0", default-features = false, features = [ "std", @@ -20,6 +20,9 @@ parking_lot = { version = "0.12.1", features = ["arc_lock"] } criterion = "0.5" timings = { path = "../timings" } +[lints.clippy] +pedantic = "warn" + [[bench]] name = "vs-arc" harness = false diff --git a/README.md b/README.md index 8b122d3..b6e943d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,38 @@ An easy-to-use, incremental, multi-threaded garbage collector for Rust. +```rust +use musegc::{CollectionGuard, Root, Ref}; + +// Execute a closure with access to a garbage collector. +musegc::collected(|| { + let mut guard = CollectionGuard::acquire(); + // Allocate a vec![Ref(1), Ref(2), Ref(3)]. + let values: Vec> = (1..=3).map(|value| Ref::new(value, &mut guard)).collect(); + let values = Root::new(values, &mut guard); + drop(guard); + + // Manually execute the garbage collector. Our data will not be freed, + // since `values` is a "root" reference. + musegc::collect(); + + // Root references allow direct access to their data, even when a + // `CollectionGuard` isn't held. + let (one, two, three) = (values[0], values[1], values[2]); + + // Accessing the data contained in a `Ref` requires a guard, however. + let mut guard = CollectionGuard::acquire(); + assert_eq!(one.load(&guard), Some(&1)); + assert_eq!(two.load(&guard), Some(&2)); + assert_eq!(three.load(&guard), Some(&3)); + + // Dropping our root will allow the collector to free our `Ref`s. + drop(values); + guard.collect(); + assert!(one.load(&guard).is_none()); +}); +``` + ## Motivation While working on [Muse][muse], @Ecton recognized [the need for garbage @@ -22,20 +54,20 @@ Tracing garbage collectors can be implemented in various ways to identify the "roots" of known memory so that they can trace from the roots through all necessary references to determine which memory allocations can be freed. -This crate exposes a `Strong` type which behaves similarly to an `Arc` but -automatically becomes a root for the collector. `Strong` implements +This crate exposes a `Root` type which behaves similarly to an `Arc` but +automatically becomes a root for the collector. `Root` implements `Deref`, allowing access to the underlying data even while the collector is running. -The `Weak` type implements `Copy` and does not provide direct access to the +The `Ref` type implements `Copy` and does not provide direct access to the underlying data. To get a reference to the underlying data, a weak reference must be upgraded using a `CollectionGuard`. The returned reference is tied to the guard, which prevents collection from running while any guards are held. A `CollectionGuard` is needed to: -- Allocate a new `Strong` or `Weak` -- Upgrade a `Weak` to an `&T` +- Allocate a new `Root` or `Ref` +- Load an `&T` from a `Ref` ## Safety @@ -61,9 +93,9 @@ triggered by incorrectly using the API or implementing the `Collectable` trait incorrectly. **Incorrect usage of this crate can lead to deadlocks and memory leaks.** Specifically: -- Reference cycles between `Strong`'s will lead to leaks just as `Arc`'s +- Reference cycles between `Root`'s will lead to leaks just as `Arc`'s will. -- If a `Strong` uses locking for interior mutability, holding a lock without +- If a `Root` uses locking for interior mutability, holding a lock without a collector guard can cause the garbage collector to block until the lock is released. This escalates from a pause to a deadlock if the lock can't be released without acquiring a collection guard. **All locks should acquired and @@ -83,8 +115,8 @@ leaks.** Specifically: Benchmarking is hard. These benchmarks aren't adequate. These numbers are from executing `benches/timings.rs`, which compares allocating 100,000 32-byte values, -comparing the time it takes to allocate each `Arc<[u8; 32]>`, `Strong<[u8;32]>`, -and `Weak<[u8; 32]>`. The measurements are the amount of time it takes for an +comparing the time it takes to allocate each `Arc<[u8; 32]>`, `Root<[u8;32]>`, +and `Ref<[u8; 32]>`. The measurements are the amount of time it takes for an individual allocation. These results are from running on a Ryzen 3700X. ### 1 thread @@ -92,40 +124,40 @@ individual allocation. These results are from running on a Ryzen 3700X. | Label | avg | min | max | stddev | out% | |--------|---------|---------|---------|---------|--------| | Arc | 47.87ns | 20.00ns | 11.27us | 151.4ns | 0.010% | -| Strong | 62.02ns | 30.00ns | 182.2us | 1.367us | 0.000% | -| Weak | 65.89ns | 30.00ns | 439.2us | 1.556us | 0.001% | +| Root | 62.02ns | 30.00ns | 182.2us | 1.367us | 0.000% | +| Ref | 65.89ns | 30.00ns | 439.2us | 1.556us | 0.001% | ### 4 threads | Label | avg | min | max | stddev | out% | |--------|---------|---------|---------|---------|--------| | Arc | 47.21ns | 20.00ns | 5.810us | 138.0ns | 0.010% | -| Strong | 147.5ns | 30.00ns | 314.4us | 3.849us | 0.001% | -| Weak | 71.03ns | 30.00ns | 633.3us | 2.464us | 0.000% | +| Root | 147.5ns | 30.00ns | 314.4us | 3.849us | 0.001% | +| Ref | 71.03ns | 30.00ns | 633.3us | 2.464us | 0.000% | ### 8 threads | Label | avg | min | max | stddev | out% | |--------|---------|---------|---------|---------|--------| | Arc | 48.91ns | 20.00ns | 65.56us | 172.4ns | 0.010% | -| Strong | 186.8ns | 30.00ns | 729.9us | 6.224us | 0.000% | -| Weak | 109.4ns | 30.00ns | 1.136ms | 5.817us | 0.000% | +| Root | 186.8ns | 30.00ns | 729.9us | 6.224us | 0.000% | +| Ref | 109.4ns | 30.00ns | 1.136ms | 5.817us | 0.000% | ### 16 threads | Label | avg | min | max | stddev | out% | |--------|---------|---------|---------|---------|--------| | Arc | 55.47ns | 20.00ns | 248.9us | 354.6ns | 0.010% | -| Strong | 323.1ns | 30.00ns | 3.105ms | 18.29us | 0.000% | -| Weak | 206.6ns | 30.00ns | 3.492ms | 16.00us | 0.000% | +| Root | 323.1ns | 30.00ns | 3.105ms | 18.29us | 0.000% | +| Ref | 206.6ns | 30.00ns | 3.492ms | 16.00us | 0.000% | ### 32 threads | Label | avg | min | max | stddev | out% | |--------|---------|---------|---------|---------|--------| | Arc | 67.12ns | 20.00ns | 260.0us | 783.3ns | 0.001% | -| Strong | 616.7ns | 30.00ns | 11.91ms | 55.58us | 0.000% | -| Weak | 432.5ns | 30.00ns | 13.66ms | 49.14us | 0.000% | +| Root | 616.7ns | 30.00ns | 11.91ms | 55.58us | 0.000% | +| Ref | 432.5ns | 30.00ns | 13.66ms | 49.14us | 0.000% | ### Author's Benchmark Summary @@ -133,7 +165,7 @@ In these benchmarks, 100 allocations are collected into a pre-allocated `Vec`. The `Vec` is cleared, and then the process is repeated 1,000 total times yielding 100,000 total allocations. -In both the `Strong` and `Weak` benchmarks, explicit calls to +In both the `Root` and `Ref` benchmarks, explicit calls to `CollectorGuard::yield_to_collector()` are placed after the `Vec` is cleared. The measurements include time waiting for the incremental garbage collector to run during these yield points. diff --git a/benches/timings.rs b/benches/timings.rs index 72d5c9b..98a3f09 100644 --- a/benches/timings.rs +++ b/benches/timings.rs @@ -1,8 +1,10 @@ +//! A multi-threaded benchmark + use std::convert::Infallible; use std::hint::black_box; use std::sync::Arc; -use musegc::{collected, CollectionGuard, Strong, Weak}; +use musegc::{collected, CollectionGuard, Ref, Root}; use timings::{Benchmark, BenchmarkImplementation, Label, LabeledTimings, Timings}; const TOTAL_ITERS: usize = 100_000; @@ -15,8 +17,8 @@ fn main() { Benchmark::default() .with_each_number_of_threads([1, 4, 8, 16, 32]) .with::() - .with::() - .with::() + .with::() + .with::() .run(&timings) .unwrap(); @@ -71,11 +73,11 @@ impl BenchmarkImplementation for StdArc { } } -struct GcWeak { +struct GcRef { metric: Label, } -impl BenchmarkImplementation for GcWeak { +impl BenchmarkImplementation for GcRef { type SharedConfig = Label; fn label(_number_of_threads: usize, _config: &()) -> Label { @@ -102,13 +104,13 @@ impl BenchmarkImplementation for GcWeak { } fn measure(&mut self, measurements: &LabeledTimings