diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..73ac2e51 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(all())'] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.config/hakari.toml b/.config/hakari.toml new file mode 100644 index 00000000..e279752e --- /dev/null +++ b/.config/hakari.toml @@ -0,0 +1,30 @@ +# This file contains settings for `cargo hakari`. +# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options. + +hakari-package = "foyer-workspace-hack" + +# Format version for hakari's output. Version 4 requires cargo-hakari 0.9.22 or above. +dep-format-version = "4" + +# Setting workspace.resolver = "2" in the root Cargo.toml is HIGHLY recommended. +# Hakari works much better with the new feature resolver. +# For more about the new feature resolver, see: +# https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#cargos-new-feature-resolver +resolver = "2" + +# Add triples corresponding to platforms commonly used by developers here. +# https://doc.rust-lang.org/rustc/platform-support.html +platforms = [ + # "x86_64-unknown-linux-gnu", + # "x86_64-apple-darwin", + # "x86_64-pc-windows-msvc", +] + +# Write out exact versions rather than a semver range. (Defaults to false.) +# exact-versions = true + +[traversal-excludes] +workspace-members = ["examples"] +third-party = [ + { name = "miniz_oxide" }, +] \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2b1d557c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "daily" + rebase-strategy: "disabled" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..3d93cffb --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## What's changed and what's your intention? + +> Please explain **IN DETAIL** what the changes are in this PR and why they are needed. :D + +## Checklist + +- [ ] I have written the necessary rustdoc comments +- [ ] I have added the necessary unit tests and integration tests +- [ ] I have passed `make all` (or `make fast` instead if the old tests are not modified) in my local environment. + +## Related issues or PRs (optional) diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 00000000..f34b2e36 --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,16 @@ +# Ref: https://github.com/zeke/semantic-pull-requests#configuration . +titleAndCommits: true +anyCommit: true +types: + - feat + - fix + - docs + - style + - refactor + - perf + - test + - build + - ci + - chore + - revert +allowMergeCommits: true \ No newline at end of file diff --git a/.github/template/generate.sh b/.github/template/generate.sh new file mode 100755 index 00000000..2d7e27c4 --- /dev/null +++ b/.github/template/generate.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# You will need to install yq >= 4.16 to use this tool. +# brew install yq + +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "$DIR" + + +HEADER=""" +# ================= THIS FILE IS AUTOMATICALLY GENERATED ================= +# +# Please run generate.sh and commit after editing the workflow templates. +# +# ======================================================================== +""" + +# Generate workflow for main branch +echo "$HEADER" > ../workflows/main.yml +# shellcheck disable=SC2016 +yq ea '. as $item ireduce ({}; . * $item )' template.yml main-override.yml | yq eval '... comments=""' - >> ../workflows/main.yml +echo "$HEADER" >> ../workflows/main.yml + +# Generate workflow for pull requests +echo "$HEADER" > ../workflows/pull-request.yml +# shellcheck disable=SC2016 +yq ea '. as $item ireduce ({}; . * $item )' template.yml pr-override.yml | yq eval '... comments=""' - >> ../workflows/pull-request.yml +echo "$HEADER" >> ../workflows/pull-request.yml + +if [ "$1" == "--check" ] ; then + if ! git diff --exit-code; then + echo "Please run generate.sh and commit after editing the workflow templates." + exit 1 + fi +fi diff --git a/.github/template/main-override.yml b/.github/template/main-override.yml new file mode 100644 index 00000000..38fd29e7 --- /dev/null +++ b/.github/template/main-override.yml @@ -0,0 +1,6 @@ +name: CI (main) + +on: + push: + branches: [main] + workflow_dispatch: diff --git a/.github/template/pr-override.yml b/.github/template/pr-override.yml new file mode 100644 index 00000000..f8c84881 --- /dev/null +++ b/.github/template/pr-override.yml @@ -0,0 +1,9 @@ +name: CI + +on: + pull_request: + branches: [main] + +concurrency: + group: environment-${{ github.ref }} + cancel-in-progress: true diff --git a/.github/template/template.yml b/.github/template/template.yml new file mode 100644 index 00000000..ccad4ddf --- /dev/null +++ b/.github/template/template.yml @@ -0,0 +1,261 @@ +name: + +on: + +env: + RUST_TOOLCHAIN: stable + RUST_TOOLCHAIN_NIGHTLY: nightly-2024-03-17 + CARGO_TERM_COLOR: always + CACHE_KEY_SUFFIX: 20240410-2 + +jobs: + misc-check: + name: misc check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run typos check + uses: crate-ci/typos@master + - name: Install yq + run: | + wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${BINARY}.tar.gz -O - | tar xz && sudo mv ${BINARY} /usr/bin/yq + env: + YQ_VERSION: v4.16.1 + BINARY: yq_linux_amd64 + BUF_VERSION: 1.0.0-rc6 + - name: Install jq + uses: dcarbone/install-jq-action@v2.0.2 + - name: Check if CI workflows are up-to-date + run: | + ./.github/template/generate.sh --check + - name: Check if Grafana dashboards are minimized + run: | + ./scripts/minimize-dashboards.sh --check + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + rust-test: + name: rust test with codecov + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy, llvm-tools-preview + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-rust-test + - name: Install cargo-binstall + if: steps.cache.outputs.cache-hit != 'true' + run: | + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + - name: Install cargo tools + if: steps.cache.outputs.cache-hit != 'true' + run: | + cargo binstall -y cargo-sort cargo-hakari + - name: Run rust cargo-sort check + run: | + cargo sort -w -c + - name: Run hakari check + run: | + cargo hakari generate --diff + cargo hakari manage-deps --dry-run + - name: Run rust format check + run: | + cargo fmt --all -- --check + - name: Run rust clippy check + run: | + cargo clippy --all-targets --features tokio-console -- -D warnings + cargo clippy --all-targets --features deadlock -- -D warnings + cargo clippy --all-targets -- -D warnings + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@cargo-llvm-cov + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@nextest + - name: Run rust test with coverage (igored tests) + run: | + cargo llvm-cov --no-report nextest --run-ignored ignored-only --no-capture --workspace + - name: Run rust test with coverage + run: | + cargo llvm-cov --no-report nextest + - name: Generate codecov report + run: | + cargo llvm-cov report --lcov --output-path lcov.info + - uses: codecov/codecov-action@v3 + if: matrix.os == 'ubuntu-latest' + with: + verbose: true + deadlock: + name: run with single worker thread and deadlock detection + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-deadlock + - name: Run foyer-storage-bench with single worker thread and deadlock detection + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo build --all --features deadlock + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/deadlock + timeout 2m ./target/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/deadlock --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + asan: + name: run with address saniziter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN_NIGHTLY }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-asan + - name: Run Unit Tests With Address Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=address --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} test --lib --bins --tests --target x86_64-unknown-linux-gnu -- --nocapture + - name: Run foyer-storage-bench With Address Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=address --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} build --all --target x86_64-unknown-linux-gnu + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/asan + timeout 2m ./target/x86_64-unknown-linux-gnu/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/asan --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + - name: Prepare Artifacts on Failure + if: ${{ failure() }} + run: |- + find ./target/x86_64-unknown-linux-gnu/debug/ -type f -executable -name 'foyer*' -print0 | xargs -0 tar czvf artifacts.asan.tgz --transform 's#.*/##' + - name: Upload Artifacts on Failure + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: artifacts.asan.tgz + path: artifacts.asan.tgz + lsan: + name: run with leak saniziter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN_NIGHTLY }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-lsan + - name: Run Unit Tests With Leak Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=leak --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} test --lib --bins --tests --target x86_64-unknown-linux-gnu -- --nocapture + - name: Run foyer-storage-bench With Leak Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=leak --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} build --all --target x86_64-unknown-linux-gnu + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/lsan + timeout 2m ./target/x86_64-unknown-linux-gnu/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/lsan --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + - name: Prepare Artifacts on Failure + if: ${{ failure() }} + run: |- + find ./target/x86_64-unknown-linux-gnu/debug/ -type f -executable -name 'foyer*' -print0 | xargs -0 tar czvf artifacts.lsan.tgz --transform 's#.*/##' + - name: Upload Artifacts on Failure + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: artifacts.lsan.tgz + path: artifacts.lsan.tgz + deterministic-test: + name: run deterministic test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-deterministic-test + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@nextest + - name: Run rust clippy check (madsim) + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable --cfg madsim" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo clippy --all-targets + - name: Run nexmark test (madsim) + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable --cfg madsim" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo nextest run --all diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml new file mode 100644 index 00000000..762f4294 --- /dev/null +++ b/.github/workflows/license_check.yml @@ -0,0 +1,19 @@ +name: License Checker + +on: + push: + branches: + - main + - "forks/*" + pull_request: + branches: + - main + - "v*.*.*-rc" +jobs: + license-header-check: + runs-on: ubuntu-latest + name: license-header-check + steps: + - uses: actions/checkout@v3 + - name: Check License Header + uses: apache/skywalking-eyes/header@df70871af1a8109c9a5b1dc824faaf65246c5236 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..2e3bac35 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,275 @@ + +# ================= THIS FILE IS AUTOMATICALLY GENERATED ================= +# +# Please run generate.sh and commit after editing the workflow templates. +# +# ======================================================================== + +name: CI (main) +on: + push: + branches: [main] + workflow_dispatch: +env: + RUST_TOOLCHAIN: stable + RUST_TOOLCHAIN_NIGHTLY: nightly-2024-03-17 + CARGO_TERM_COLOR: always + CACHE_KEY_SUFFIX: 20240410-2 +jobs: + misc-check: + name: misc check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run typos check + uses: crate-ci/typos@master + - name: Install yq + run: | + wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${BINARY}.tar.gz -O - | tar xz && sudo mv ${BINARY} /usr/bin/yq + env: + YQ_VERSION: v4.16.1 + BINARY: yq_linux_amd64 + BUF_VERSION: 1.0.0-rc6 + - name: Install jq + uses: dcarbone/install-jq-action@v2.0.2 + - name: Check if CI workflows are up-to-date + run: | + ./.github/template/generate.sh --check + - name: Check if Grafana dashboards are minimized + run: | + ./scripts/minimize-dashboards.sh --check + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + rust-test: + name: rust test with codecov + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy, llvm-tools-preview + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-rust-test + - name: Install cargo-binstall + if: steps.cache.outputs.cache-hit != 'true' + run: | + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + - name: Install cargo tools + if: steps.cache.outputs.cache-hit != 'true' + run: | + cargo binstall -y cargo-sort cargo-hakari + - name: Run rust cargo-sort check + run: | + cargo sort -w -c + - name: Run hakari check + run: | + cargo hakari generate --diff + cargo hakari manage-deps --dry-run + - name: Run rust format check + run: | + cargo fmt --all -- --check + - name: Run rust clippy check + run: | + cargo clippy --all-targets --features tokio-console -- -D warnings + cargo clippy --all-targets --features deadlock -- -D warnings + cargo clippy --all-targets -- -D warnings + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@cargo-llvm-cov + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@nextest + - name: Run rust test with coverage (igored tests) + run: | + cargo llvm-cov --no-report nextest --run-ignored ignored-only --no-capture --workspace + - name: Run rust test with coverage + run: | + cargo llvm-cov --no-report nextest + - name: Generate codecov report + run: | + cargo llvm-cov report --lcov --output-path lcov.info + - uses: codecov/codecov-action@v3 + if: matrix.os == 'ubuntu-latest' + with: + verbose: true + deadlock: + name: run with single worker thread and deadlock detection + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-deadlock + - name: Run foyer-storage-bench with single worker thread and deadlock detection + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo build --all --features deadlock + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/deadlock + timeout 2m ./target/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/deadlock --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + asan: + name: run with address saniziter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN_NIGHTLY }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-asan + - name: Run Unit Tests With Address Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=address --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} test --lib --bins --tests --target x86_64-unknown-linux-gnu -- --nocapture + - name: Run foyer-storage-bench With Address Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=address --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} build --all --target x86_64-unknown-linux-gnu + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/asan + timeout 2m ./target/x86_64-unknown-linux-gnu/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/asan --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + - name: Prepare Artifacts on Failure + if: ${{ failure() }} + run: |- + find ./target/x86_64-unknown-linux-gnu/debug/ -type f -executable -name 'foyer*' -print0 | xargs -0 tar czvf artifacts.asan.tgz --transform 's#.*/##' + - name: Upload Artifacts on Failure + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: artifacts.asan.tgz + path: artifacts.asan.tgz + lsan: + name: run with leak saniziter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN_NIGHTLY }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-lsan + - name: Run Unit Tests With Leak Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=leak --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} test --lib --bins --tests --target x86_64-unknown-linux-gnu -- --nocapture + - name: Run foyer-storage-bench With Leak Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=leak --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} build --all --target x86_64-unknown-linux-gnu + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/lsan + timeout 2m ./target/x86_64-unknown-linux-gnu/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/lsan --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + - name: Prepare Artifacts on Failure + if: ${{ failure() }} + run: |- + find ./target/x86_64-unknown-linux-gnu/debug/ -type f -executable -name 'foyer*' -print0 | xargs -0 tar czvf artifacts.lsan.tgz --transform 's#.*/##' + - name: Upload Artifacts on Failure + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: artifacts.lsan.tgz + path: artifacts.lsan.tgz + deterministic-test: + name: run deterministic test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-deterministic-test + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@nextest + - name: Run rust clippy check (madsim) + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable --cfg madsim" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo clippy --all-targets + - name: Run nexmark test (madsim) + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable --cfg madsim" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo nextest run --all + +# ================= THIS FILE IS AUTOMATICALLY GENERATED ================= +# +# Please run generate.sh and commit after editing the workflow templates. +# +# ======================================================================== + diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 00000000..a8b0fd06 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,277 @@ + +# ================= THIS FILE IS AUTOMATICALLY GENERATED ================= +# +# Please run generate.sh and commit after editing the workflow templates. +# +# ======================================================================== + +name: CI +on: + pull_request: + branches: [main] +env: + RUST_TOOLCHAIN: stable + RUST_TOOLCHAIN_NIGHTLY: nightly-2024-03-17 + CARGO_TERM_COLOR: always + CACHE_KEY_SUFFIX: 20240410-2 +jobs: + misc-check: + name: misc check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run typos check + uses: crate-ci/typos@master + - name: Install yq + run: | + wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${BINARY}.tar.gz -O - | tar xz && sudo mv ${BINARY} /usr/bin/yq + env: + YQ_VERSION: v4.16.1 + BINARY: yq_linux_amd64 + BUF_VERSION: 1.0.0-rc6 + - name: Install jq + uses: dcarbone/install-jq-action@v2.0.2 + - name: Check if CI workflows are up-to-date + run: | + ./.github/template/generate.sh --check + - name: Check if Grafana dashboards are minimized + run: | + ./scripts/minimize-dashboards.sh --check + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + rust-test: + name: rust test with codecov + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy, llvm-tools-preview + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-rust-test + - name: Install cargo-binstall + if: steps.cache.outputs.cache-hit != 'true' + run: | + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + - name: Install cargo tools + if: steps.cache.outputs.cache-hit != 'true' + run: | + cargo binstall -y cargo-sort cargo-hakari + - name: Run rust cargo-sort check + run: | + cargo sort -w -c + - name: Run hakari check + run: | + cargo hakari generate --diff + cargo hakari manage-deps --dry-run + - name: Run rust format check + run: | + cargo fmt --all -- --check + - name: Run rust clippy check + run: | + cargo clippy --all-targets --features tokio-console -- -D warnings + cargo clippy --all-targets --features deadlock -- -D warnings + cargo clippy --all-targets -- -D warnings + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@cargo-llvm-cov + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@nextest + - name: Run rust test with coverage (igored tests) + run: | + cargo llvm-cov --no-report nextest --run-ignored ignored-only --no-capture --workspace + - name: Run rust test with coverage + run: | + cargo llvm-cov --no-report nextest + - name: Generate codecov report + run: | + cargo llvm-cov report --lcov --output-path lcov.info + - uses: codecov/codecov-action@v3 + if: matrix.os == 'ubuntu-latest' + with: + verbose: true + deadlock: + name: run with single worker thread and deadlock detection + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-deadlock + - name: Run foyer-storage-bench with single worker thread and deadlock detection + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo build --all --features deadlock + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/deadlock + timeout 2m ./target/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/deadlock --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + asan: + name: run with address saniziter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN_NIGHTLY }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-asan + - name: Run Unit Tests With Address Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=address --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} test --lib --bins --tests --target x86_64-unknown-linux-gnu -- --nocapture + - name: Run foyer-storage-bench With Address Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=address --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} build --all --target x86_64-unknown-linux-gnu + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/asan + timeout 2m ./target/x86_64-unknown-linux-gnu/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/asan --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + - name: Prepare Artifacts on Failure + if: ${{ failure() }} + run: |- + find ./target/x86_64-unknown-linux-gnu/debug/ -type f -executable -name 'foyer*' -print0 | xargs -0 tar czvf artifacts.asan.tgz --transform 's#.*/##' + - name: Upload Artifacts on Failure + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: artifacts.asan.tgz + path: artifacts.asan.tgz + lsan: + name: run with leak saniziter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN_NIGHTLY }} + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-lsan + - name: Run Unit Tests With Leak Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=leak --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} test --lib --bins --tests --target x86_64-unknown-linux-gnu -- --nocapture + - name: Run foyer-storage-bench With Leak Sanitizer + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Zsanitizer=leak --cfg tokio_unstable" + RUST_LOG: info + run: |- + cargo +${{ env.RUST_TOOLCHAIN_NIGHTLY }} build --all --target x86_64-unknown-linux-gnu + mkdir -p $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/lsan + timeout 2m ./target/x86_64-unknown-linux-gnu/debug/foyer-storage-bench --dir $GITHUB_WORKSPACE/foyer-data/foyer-storage-bench/lsan --capacity 256 --region-size 16 --lookup-range 1000 --w-rate 1 --r-rate 1 --ticket-insert-rate-limit 10 --time 60 + - name: Prepare Artifacts on Failure + if: ${{ failure() }} + run: |- + find ./target/x86_64-unknown-linux-gnu/debug/ -type f -executable -name 'foyer*' -print0 | xargs -0 tar czvf artifacts.lsan.tgz --transform 's#.*/##' + - name: Upload Artifacts on Failure + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: artifacts.lsan.tgz + path: artifacts.lsan.tgz + deterministic-test: + name: run deterministic test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Install rust toolchain@v1 + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy + - name: Cache Cargo home + uses: actions/cache@v2 + id: cache + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{ env.CACHE_KEY_SUFFIX }}-deterministic-test + - if: steps.cache.outputs.cache-hit != 'true' + uses: taiki-e/install-action@nextest + - name: Run rust clippy check (madsim) + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable --cfg madsim" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo clippy --all-targets + - name: Run nexmark test (madsim) + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "--cfg tokio_unstable --cfg madsim" + RUST_LOG: info + TOKIO_WORKER_THREADS: 1 + run: |- + cargo nextest run --all +concurrency: + group: environment-${{ github.ref }} + cancel-in-progress: true + +# ================= THIS FILE IS AUTOMATICALLY GENERATED ================= +# +# Please run generate.sh and commit after editing the workflow templates. +# +# ======================================================================== + diff --git a/.gitignore b/.gitignore index ea8c4bf7..769a4793 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,14 @@ +/.vscode + /target + +foyer-memory/fuzz/artifacts/cache_fuzz/ +foyer-memory/fuzz/corpus/cache_fuzz + +Cargo.lock +lcov.info + +docker-compose.override.yaml +.tmp + +perf.data* \ No newline at end of file diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 00000000..bfb4ceb2 --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,9 @@ +header: + license: + spdx-id: Apache-2.0 + copyright-owner: Foyer Project Authors + + paths: + - "**/*.rs" + + comment: on-failure diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..d843d98a --- /dev/null +++ b/.typos.toml @@ -0,0 +1,8 @@ +[default.extend-words] + +[default.extend-identifiers] + +[files] +extend-exclude = [ + "foyer-workspace-hack/Cargo.toml", +] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3e656e0b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,245 @@ +## 2024-04-11 + +| crate | version | +| - | - | +| foyer | 0.7.0 | +| foyer-common | 0.5.0 | +| foyer-intrusive | 0.4.0 | +| foyer-memory | 0.2.0 | +| foyer-storage | 0.6.0 | +| foyer-storage-bench | 0.6.0 | +| foyer-workspace-hack | 0.4.0 | + +
+ +### Changes + +- Make `foyer` compatible with rust stable toolchain (MSRV = 1.77.2). 🎉 + +
+ +## 2024-04-09 + +| crate | version | +| - | - | +| foyer-storage | 0.5.1 | +| foyer-memory | 0.1.4 | + +
+ +### Changes + +- fix: Fix panics on `state()` for s3fifo entry. +- fix: Enable `offset_of` feature for `foyer-storage`. + +
+ +## 2024-04-08 + +| crate | version | +| - | - | +| foyer-intrusive | 0.3.1 | +| foyer-memory | 0.1.3 | + +
+ +### Changes + +- feat: Introduce s3fifo to `foyer-memory`. +- fix: Fix doctest for `foyer-intrusive`. + +
+ +## 2024-03-21 + +| crate | version | +| - | - | +| foyer-memory | 0.1.2 | + +
+ +### Changes + +- fix: `foyer-memory` export `DefaultCacheEventListener`. + +
+ +## 2024-03-14 + +| crate | version | +| - | - | +| foyer-memory | 0.1.1 | + +
+ +### Changes + +- Make eviction config clonable. + +
+ +## 2024-03-13 + +| crate | version | +| - | - | +| foyer-storage-bench | 0.5.1 | + +
+ +### Changes + +- Fix `foyer-storage-bench` build with `trace` feature. + +
+ +## 2024-03-12 + +| crate | version | +| - | - | +| foyer | 0.6.0 | +| foyer-common | 0.4.0 | +| foyer-intrusive | 0.3.0 | +| foyer-memory | 0.1.0 | +| foyer-storage | 0.5.0 | +| foyer-storage-bench | 0.5.0 | +| foyer-workspace-hack | 0.3.0 | + +
+ +### Changes + +- Release foyer in-memory cache as crate `foyer-memory`. +- Bump other components with changes. + +
+ +## 2023-12-28 + +| crate | version | +| - | - | +| foyer | 0.5.0 | +| foyer-common | 0.3.0 | +| foyer-intrusive | 0.2.0 | +| foyer-storage | 0.4.0 | +| foyer-storage-bench | 0.4.0 | +| foyer-workspace-hack | 0.2.0 | + +
+ +### Changes + +- Bump rust-toolchain to "nightly-2023-12-26". +- Introduce time-series distribution args to bench tool. [#253](https://github.com/MrCroxx/foyer/pull/253) + +### Fixes + +- Fix duplicated insert drop metrics. + +
+ +## 2023-12-22 + +| crate | version | +| - | - | +| foyer | 0.4.0 | +| foyer-storage | 0.3.0 | +| foyer-storage-bench | 0.3.0 | +| foyer-workspace-hack | 0.1.1 | + +
+ +### Changes + +- Remove config `flusher_buffer_capacity`. + +### Fixes + +- Fix benchmark tool cache miss ratio. + +
+ +## 2023-12-20 + +| crate | version | +| - | - | +| foyer-storage | 0.2.2 | + +
+ +- Fix metrics for writer dropping. +- Add interface `insert_async_with_callback` and `insert_if_not_exists_async_with_callback` for callers to get the insert result. + +
+ +## 2023-12-18 + +| crate | version | +| - | - | +| foyer-storage | 0.2.1 | + +
+ +- Introduce the entry size histogram, update metrics. + +
+ +## 2023-12-18 + +| crate | version | +| - | - | +| foyer | 0.3.0 | +| foyer-common | 0.2.0 | +| foyer-storage | 0.2.0 | +| foyer-storage-bench | 0.2.0 | + +
+ +- Introduce the associated type `Cursor` for trait `Key` and `Value` to reduce unnecessary buffer copy if possible. +- Remove the ring buffer and continuum tracker for they are no longer needed. +- Update the configuration of the storage engine and the benchmark tool. + +
+ +## 2023-11-29 + +| crate | version | +| - | - | +| foyer | 0.2.0 | +| foyer-common | 0.1.0 | +| foyer-intrusive | 0.1.0 | +| foyer-storage | 0.1.0 | +| foyer-storage-bench | 0.1.0 | +| foyer-workspace-hack | 0.1.0 | + +
+ +The first version that can be used as file cache. + +The write model and the design of storage engine has been switched from CacheLib navy version to the ring buffer version (which was highly inspired by MySQL 8.0 link_buf). + +Introduces `Store`, `RuntimeStore`, `LazyStore` to simplify usage. In most cases, `RuntimeStore` is preferred to use a dedicated tokio runtime to serve **foyer** to avoid the influence to the user's runtime. If lazy-load is needed, use `RuntimeLazyStore` instead. + +The implementation of **foyer** is separated into multiple crates. But importing `foyer` is enough for it re-exports the crates that **foyer**'s user needs. + +Brief description about the subcrates: + +- foyer-common: Provide basic data structures and algorithms. +- foyer-intrusive: Provide intrusive containers for implementing eviction lists and collections. Intrisive data structures provide the ability to implement low-cost multi-index data structures, which will be used for the memory cache in future. +- foyer-storage: Provide the file cache storage engine and wrappers to simplify the code. +- foyer-storage-bench: Runnable benchmark tool for the file cache storage engine. +- foyer-workspace-hack: Generated by [hakari](https://crates.io/crates/hakari) to prevent building each crate from triggering building from scratch. + +
+ + +## 2023-05-12 + +| crate | version | +| - | - | +| foyer | 0.1.0 | + +
+ +Initial version with just bacis interfaces. + +
diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index afea6a3a..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,362 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "crossbeam-epoch" -version = "0.9.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "foyer" -version = "0.1.0" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "futures", - "parking_lot", - "paste", - "rand_mt", -] - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "libc" -version = "0.2.144" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memoffset" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" -dependencies = [ - "autocfg", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro2" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "rand_mt" -version = "4.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77717f0c31ae3827865b59615cebc0e54374bd802bfe0ef067684a50bcfb10a9" -dependencies = [ - "rand_core", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "syn" -version = "2.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml index af4da56c..dc165ffe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,29 @@ -[package] -name = "foyer" -version = "0.1.0" -edition = "2021" -authors = ["MrCroxx "] -description = "Hybrid cache for Rust" -license = "Apache-2.0" +[workspace] +resolver = "2" +members = [ + "examples", + "foyer", + "foyer-common", + "foyer-experimental", + "foyer-experimental-bench", + "foyer-intrusive", + "foyer-memory", + "foyer-memory/fuzz", + "foyer-storage", + "foyer-storage-bench", + "foyer-workspace-hack", +] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace.dependencies] +tokio = { package = "madsim-tokio", version = "0.2", features = [ + "rt", + "rt-multi-thread", + "sync", + "macros", + "time", + "signal", + "fs", +] } -[dependencies] -crossbeam-epoch = "0.9" -futures = "0.3" -parking_lot = "0.12" -paste = "1.0" - -[dev-dependencies] -crossbeam-utils = "0.8" -rand_mt = "4.2.1" +[profile.release] +debug = "full" diff --git a/LICENSE b/LICENSE index f5ef119f..7e01351a 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 MrCroxx + Copyright 2024 Foyer Project Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..72c61773 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +SHELL := /bin/bash +.PHONY: deps check test test-ignored test-all all fast monitor clear madsim + +deps: + ./scripts/install-deps.sh + +check: + typos + shellcheck ./scripts/* + ./.github/template/generate.sh + ./scripts/minimize-dashboards.sh + cargo hakari generate + cargo hakari manage-deps + cargo sort -w + cargo fmt --all + cargo clippy --all-targets + # TODO(MrCroxx): Restore udeps check after it doesn't requires nightly anymore. + # cargo udeps --workspace --exclude foyer-workspace-hack + +check-all: + shellcheck ./scripts/* + ./.github/template/generate.sh + ./scripts/minimize-dashboards.sh + cargo hakari generate + cargo hakari manage-deps + cargo sort -w + cargo fmt --all + cargo clippy --all-targets --features deadlock + cargo clippy --all-targets --features tokio-console + cargo clippy --all-targets --features trace + cargo clippy --all-targets + # TODO(MrCroxx): Restore udeps check after it doesn't requires nightly anymore. + # cargo udeps --workspace --exclude foyer-workspace-hack + +test: + RUST_BACKTRACE=1 cargo nextest run --all + RUST_BACKTRACE=1 cargo test --doc + +test-ignored: + RUST_BACKTRACE=1 cargo nextest run --run-ignored ignored-only --no-capture --workspace + +test-all: test test-ignored + +madsim: + RUSTFLAGS="--cfg madsim --cfg tokio_unstable" cargo clippy --all-targets + RUSTFLAGS="--cfg madsim --cfg tokio_unstable" RUST_BACKTRACE=1 cargo nextest run --all + RUSTFLAGS="--cfg madsim --cfg tokio_unstable" RUST_BACKTRACE=1 cargo test --doc + +all: check-all test-all + +fast: check test + +monitor: + ./scripts/monitor.sh + +clear: + rm -rf .tmp diff --git a/README.md b/README.md new file mode 100644 index 00000000..ff259e56 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# foyer + +![Crates.io Version](https://img.shields.io/crates/v/foyer) +![Crates.io MSRV](https://img.shields.io/crates/msrv/foyer) +![GitHub License](https://img.shields.io/github/license/mrcroxx/foyer) + +[![CI (main)](https://github.com/MrCroxx/foyer/actions/workflows/main.yml/badge.svg)](https://github.com/MrCroxx/foyer/actions/workflows/main.yml) +[![License Checker](https://github.com/MrCroxx/foyer/actions/workflows/license_check.yml/badge.svg)](https://github.com/MrCroxx/foyer/actions/workflows/license_check.yml) +[![codecov](https://codecov.io/github/MrCroxx/foyer/branch/main/graph/badge.svg?token=YO33YQCB70)](https://codecov.io/github/MrCroxx/foyer) + +*foyer* aims to be a user-friendly hybrid cache lib in Rust. + +*foyer* is inspired by [Facebook/CacheLib](https://github.com/facebook/cachelib), which is an excellent hybrid cache lib in C++. *foyer* is not only a 'rewrite in Rust project', but provide some features that *CacheLib* doesn't have for now. + +## Supported Rust Versions + +*foyer* is built against the latest stable release. The minimum supported version is 1.77.2. The current *foyer* version is not guaranteed to build on Rust versions earlier than the minimum supported version. + +## Development state + +Currently, *foyer* only finished few features, and is still under heavy development. + +## Features + +- [x] in-memory cache + - [x] FIFO + - [x] LRU with priority pool + - [x] 3-qeue w-TinyLFU (imspired by [caffeine](https://github.com/ben-manes/caffeine)) + - [x] S3FIFO without Ghost Queue +- [x] disk cache +- [ ] TTL (time to live) + +## Examples + +The examples can be found [here](https://github.com/MrCroxx/foyer/tree/main/examples). + +## Roadmap + +- [ ] More user-friendly API. +- [ ] User-friendly Documents and examples. +- [ ] Support TTL. +- [ ] Simplify `foyer-storage`. +- [ ] Refactor `foyer-storage` region reclaiming policy. +- [ ] Support on Windows. +- [ ] Unify in-memory cache and disk cache configuration. + +## Contributing + +Contributions for *foyer* are welcomed! Issues can be found [here](https://github.com/MrCroxx/foyer/issues). + +Make sure you've passed `make check` and `make test` before request a review, or CI will fail. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..0907d437 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,25 @@ +# codecov config +# Reference: https://docs.codecov.com/docs/codecovyml-reference +# Tips. You may run following command to validate before committing any changes +# curl --data-binary @codecov.yml https://codecov.io/validate +coverage: + precision: 2 + round: down + range: "70..100" + status: + project: + default: + target: auto + threshold: "1%" + only_pulls: true + patch: + default: + informational: true + only_pulls: true + changes: + default: + informational: true + only_pulls: true +ignore: + - "foyer-storage-bench" + - "foyer-experimental-bench" diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..7a46ae9c --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,60 @@ +version: "3" + +services: + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + restart: unless-stopped + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.rootfs=/rootfs' + - '--path.sysfs=/host/sys' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + ports: + - 9100:9100 + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + volumes: + - ./etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - ./.tmp/prometheus:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.enable-lifecycle' + ports: + - 9090:9090 + extra_hosts: + - "host.docker.internal:host-gateway" + + grafana: + image: grafana/grafana:latest + container_name: grafana + volumes: + - ./etc/grafana/grafana.ini:/etc/grafana/grafana.ini + - ./etc/grafana/provisioning:/etc/grafana/provisioning + - ./etc/grafana/dashboards:/var/lib/grafana/dashboards + ports: + - 3000:3000 + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: jaeger + command: + - '--collector.otlp.enabled=true' + ports: + - 6831:6831/udp + - 6832:6832/udp + - 16686:16686 + - 4317:4317 + diff --git a/etc/grafana/dashboards/foyer.json b/etc/grafana/dashboards/foyer.json new file mode 100644 index 00000000..bf43af6e --- /dev/null +++ b/etc/grafana/dashboards/foyer.json @@ -0,0 +1 @@ +{"annotations":{"list":[{"builtIn":1,"datasource":{"type":"grafana","uid":"-- Grafana --"},"enable":true,"hide":true,"iconColor":"rgba(0, 211, 255, 1)","name":"Annotations & Alerts","type":"dashboard"}]},"editable":true,"fiscalYearStartMonth":0,"graphTooltip":1,"links":[],"liveNow":false,"panels":[{"collapsed":false,"gridPos":{"h":1,"w":24,"x":0,"y":0},"id":8,"panels":[],"title":"Storage","type":"row"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"ops"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":1},"id":1,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"sum(rate(foyer_storage_op_duration_count[$__rate_interval])) by (foyer, op, extra)","instant":false,"legendFormat":"{{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"}],"title":"Op","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"ops"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":1},"id":2,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"sum(rate(foyer_storage_slow_op_duration_count[$__rate_interval])) by (foyer, op, extra)","instant":false,"legendFormat":"{{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"}],"title":"Slow Op","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":9},"id":3,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.5, sum(rate(foyer_storage_op_duration_bucket{op=~\"insert|remove\"}[$__rate_interval])) by (le, foyer, op, extra)) ","instant":false,"legendFormat":"p50 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.9, sum(rate(foyer_storage_op_duration_bucket{op=~\"insert|remove\"}[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p90 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"B"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.99, sum(rate(foyer_storage_op_duration_bucket{op=~\"insert|remove\"}[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p99 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"C"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(1.0, sum(rate(foyer_storage_op_duration_bucket{op=~\"insert|remove\"}[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"pmax - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"D"}],"title":"Op Write Duration","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":9},"id":10,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.5, sum(rate(foyer_storage_op_duration_bucket{op=~\"lookup\"}[$__rate_interval])) by (le, foyer, op, extra)) ","instant":false,"legendFormat":"p50 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.9, sum(rate(foyer_storage_op_duration_bucket{op=~\"lookup\"}[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p90 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"B"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.99, sum(rate(foyer_storage_op_duration_bucket{op=~\"lookup\"}[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p99 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"C"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(1.0, sum(rate(foyer_storage_op_duration_bucket{op=~\"lookup\"}[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"pmax - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"D"}],"title":"Op Read Duration","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"Bps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":17},"id":5,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"sum(rate(foyer_storage_op_bytes[$__rate_interval])) by (foyer, op, extra) ","instant":false,"legendFormat":"{{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"}],"title":"Op Thoughput","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":17},"id":7,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"sum(rate(foyer_storage_op_duration_count{op=\"lookup\", extra=\"hit\"}[$__rate_interval])) by (foyer) / (sum(rate(foyer_storage_op_duration_count{op=\"lookup\", extra=\"hit\"}[$__rate_interval])) by (foyer) + sum(rate(foyer_storage_op_duration_count{op=\"lookup\", extra=\"miss\"}[$__rate_interval])) by (foyer)) ","instant":false,"legendFormat":"{{foyer}} foyer storage","range":true,"refId":"A"}],"title":"Hit Ratio","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":25},"id":9,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.5, sum(rate(foyer_storage_inner_op_duration_bucket [$__rate_interval])) by (le, foyer, op, extra)) ","instant":false,"legendFormat":"p50 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.9, sum(rate(foyer_storage_inner_op_duration_bucket [$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p90 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"B"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.99, sum(rate(foyer_storage_inner_op_duration_bucket [$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p99 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"C"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(1.0, sum(rate(foyer_storage_inner_op_duration_bucket [$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"pmax - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"D"}],"title":"Inner Op Duration","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":25},"id":4,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.5, sum(rate(foyer_storage_slow_op_duration_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","instant":false,"legendFormat":"p50 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.9, sum(rate(foyer_storage_slow_op_duration_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p90 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"B"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.99, sum(rate(foyer_storage_slow_op_duration_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p99 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"C"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(1.0, sum(rate(foyer_storage_slow_op_duration_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"pmax - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"D"}],"title":"Slow Op Duration","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":33},"id":12,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.5, sum(rate(foyer_storage_entry_bytes_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","instant":false,"legendFormat":"p50 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"A"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.9, sum(rate(foyer_storage_entry_bytes_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p90 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"B"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(0.99, sum(rate(foyer_storage_entry_bytes_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"p99 - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"C"},{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"histogram_quantile(1.0, sum(rate(foyer_storage_entry_bytes_bucket[$__rate_interval])) by (le, foyer, op, extra)) ","hide":false,"instant":false,"legendFormat":"pmax - {{foyer}} foyer storage - {{op}} {{extra}}","range":true,"refId":"D"}],"title":"Entry Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":33},"id":11,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"sum(foyer_storage_inner_bytes) by (foyer, component, extra) ","instant":false,"legendFormat":"{{foyer}} {{component}} {{extra}}","range":true,"refId":"A"}],"title":"Inner Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":10,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":41},"id":6,"options":{"legend":{"calcs":["lastNotNull"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"targets":[{"datasource":{"type":"prometheus","uid":"a2641a73-8591-446b-9d69-7869ebf43899"},"editorMode":"code","expr":"sum(foyer_storage_total_bytes) by (foyer) ","instant":false,"legendFormat":"{{foyer}} foyer storage","range":true,"refId":"A"}],"title":"Size","type":"timeseries"}],"refresh":"5s","schemaVersion":38,"style":"dark","tags":[],"templating":{"list":[]},"time":{"from":"now-30m","to":"now"},"timepicker":{},"timezone":"","title":"foyer","uid":"f0e2058b-b292-457c-8ddf-9dbdf7c60035","version":1,"weekStart":""} diff --git a/etc/grafana/dashboards/node-exporter-full.json b/etc/grafana/dashboards/node-exporter-full.json new file mode 100644 index 00000000..a782ea1d --- /dev/null +++ b/etc/grafana/dashboards/node-exporter-full.json @@ -0,0 +1 @@ +{"annotations":{"list":[{"$$hashKey":"object:1058","builtIn":1,"datasource":{"type":"datasource","uid":"grafana"},"enable":true,"hide":true,"iconColor":"rgba(0, 211, 255, 1)","name":"Annotations & Alerts","target":{"limit":100,"matchAny":false,"tags":[],"type":"dashboard"},"type":"dashboard"}]},"editable":true,"fiscalYearStartMonth":0,"gnetId":1860,"graphTooltip":1,"id":2,"links":[{"icon":"external link","tags":[],"targetBlank":true,"title":"GitHub","type":"link","url":"https://github.com/rfmoz/grafana-dashboards"},{"icon":"external link","tags":[],"targetBlank":true,"title":"Grafana","type":"link","url":"https://grafana.com/grafana/dashboards/1860"}],"liveNow":false,"panels":[{"collapsed":false,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":0},"id":261,"panels":[],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Quick CPU / Mem / Disk","type":"row"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Busy state of all CPU cores together","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":85},{"color":"rgba(245, 54, 54, 0.9)","value":95}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":4,"w":3,"x":0,"y":1},"id":20,"links":[],"options":{"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"(sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode!=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))) * 100","hide":false,"instant":true,"intervalFactor":1,"legendFormat":"","range":false,"refId":"A","step":240}],"title":"CPU Busy","type":"gauge"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Busy state of all CPU cores together (5 min average)","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":85},{"color":"rgba(245, 54, 54, 0.9)","value":95}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":4,"w":3,"x":3,"y":1},"id":155,"links":[],"options":{"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"avg_over_time(node_load5{instance=\"$node\",job=\"$job\"}[$__rate_interval]) * 100 / on(instance) group_left sum by (instance)(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]))","format":"time_series","hide":false,"instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"Sys Load (5m avg)","type":"gauge"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Busy state of all CPU cores together (15 min average)","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":85},{"color":"rgba(245, 54, 54, 0.9)","value":95}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":4,"w":3,"x":6,"y":1},"id":19,"links":[],"options":{"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"avg_over_time(node_load15{instance=\"$node\",job=\"$job\"}[$__rate_interval]) * 100 / on(instance) group_left sum by (instance)(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]))","hide":false,"instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"Sys Load (15m avg)","type":"gauge"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Non available RAM memory","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":80},{"color":"rgba(245, 54, 54, 0.9)","value":90}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":4,"w":3,"x":9,"y":1},"hideTimeOverride":false,"id":16,"links":[],"options":{"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"((avg_over_time(node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval]) - avg_over_time(node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])) / (avg_over_time(node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval]) )) * 100","format":"time_series","hide":true,"instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"100 - ((avg_over_time(node_memory_MemAvailable_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval]) * 100) / avg_over_time(node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval]))","format":"time_series","hide":false,"instant":true,"intervalFactor":1,"range":false,"refId":"B","step":240}],"title":"RAM Used","type":"gauge"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Used Swap","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":10},{"color":"rgba(245, 54, 54, 0.9)","value":25}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":4,"w":3,"x":12,"y":1},"id":21,"links":[],"options":{"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"((avg_over_time(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval]) - avg_over_time(node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])) / (avg_over_time(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval]) )) * 100","instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"SWAP Used","type":"gauge"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Used Root FS","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":80},{"color":"rgba(245, 54, 54, 0.9)","value":90}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":4,"w":3,"x":15,"y":1},"id":154,"links":[],"options":{"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"100 - ((avg_over_time(node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"}[$__rate_interval]) * 100) / avg_over_time(node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"}[$__rate_interval]))","format":"time_series","instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"Root FS Used","type":"gauge"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Total number of CPU cores","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":2,"w":2,"x":18,"y":1},"id":14,"links":[],"maxDataPoints":100,"options":{"colorMode":"none","graphMode":"none","justifyMode":"auto","orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))","legendFormat":"__auto","range":true,"refId":"A"}],"title":"CPU Cores","type":"stat"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"System uptime","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":1,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":2,"w":4,"x":20,"y":1},"hideTimeOverride":true,"id":15,"links":[],"maxDataPoints":100,"options":{"colorMode":"none","graphMode":"none","justifyMode":"auto","orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"node_time_seconds{instance=\"$node\",job=\"$job\"} - node_boot_time_seconds{instance=\"$node\",job=\"$job\"}","instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"Uptime","type":"stat"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Total RootFS","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":0,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"thresholds":{"mode":"absolute","steps":[{"color":"rgba(50, 172, 45, 0.97)","value":null},{"color":"rgba(237, 129, 40, 0.89)","value":70},{"color":"rgba(245, 54, 54, 0.9)","value":90}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":2,"w":2,"x":18,"y":3},"id":23,"links":[],"maxDataPoints":100,"options":{"colorMode":"none","graphMode":"none","justifyMode":"auto","orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"}","format":"time_series","hide":false,"instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"RootFS Total","type":"stat"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Total RAM","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":0,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":2,"w":2,"x":20,"y":3},"id":75,"links":[],"maxDataPoints":100,"options":{"colorMode":"none","graphMode":"none","justifyMode":"auto","orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}","instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"RAM Total","type":"stat"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Total SWAP","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"decimals":0,"mappings":[{"options":{"match":"null","result":{"text":"N/A"}},"type":"special"}],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":2,"w":2,"x":22,"y":3},"id":18,"links":[],"maxDataPoints":100,"options":{"colorMode":"none","graphMode":"none","justifyMode":"auto","orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.1.1","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","exemplar":false,"expr":"node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"}","instant":true,"intervalFactor":1,"range":false,"refId":"A","step":240}],"title":"SWAP Total","type":"stat"},{"collapsed":false,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":5},"id":263,"panels":[],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Basic CPU / Mem / Net / Disk","type":"row"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Basic CPU info","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"percent"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"percentunit"},"overrides":[{"matcher":{"id":"byName","options":"Busy Iowait"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Idle"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Busy Iowait"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Idle"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Busy System"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Busy User"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Busy Other"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]}]},"gridPos":{"h":7,"w":12,"x":0,"y":6},"id":77,"links":[],"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true,"width":250},"tooltip":{"mode":"multi","sort":"desc"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Busy System","range":true,"refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Busy User","range":true,"refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Busy Iowait","range":true,"refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=~\".*irq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Busy IRQs","range":true,"refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode!='idle',mode!='user',mode!='system',mode!='iowait',mode!='irq',mode!='softirq'}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Busy Other","range":true,"refId":"E","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Idle","range":true,"refId":"F","step":240}],"title":"CPU Basic","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Basic memory usage","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"SWAP Used"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap Used"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM Total"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}},{"id":"custom.fillOpacity","value":0},{"id":"custom.stacking","value":{"group":false,"mode":"normal"}}]},{"matcher":{"id":"byName","options":"RAM Cache + Buffer"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM Free"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Available"},"properties":[{"id":"color","value":{"fixedColor":"#DEDAF7","mode":"fixed"}},{"id":"custom.fillOpacity","value":0},{"id":"custom.stacking","value":{"group":false,"mode":"normal"}}]}]},"gridPos":{"h":7,"w":12,"x":12,"y":6},"id":78,"links":[],"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"RAM Total","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - (node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"})","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"RAM Used","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"RAM Cache + Buffer","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"RAM Free","refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})","format":"time_series","intervalFactor":1,"legendFormat":"SWAP Used","refId":"E","step":240}],"title":"Memory Basic","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Basic network info per interface","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"bps"},"overrides":[{"matcher":{"id":"byName","options":"Recv_bytes_eth2"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Recv_bytes_lo"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Recv_drop_eth2"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Recv_drop_lo"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Recv_errs_eth2"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Recv_errs_lo"},"properties":[{"id":"color","value":{"fixedColor":"#CCA300","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Trans_bytes_eth2"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Trans_bytes_lo"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Trans_drop_eth2"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Trans_drop_lo"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Trans_errs_eth2"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Trans_errs_lo"},"properties":[{"id":"color","value":{"fixedColor":"#CCA300","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"recv_bytes_lo"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"recv_drop_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"recv_drop_lo"},"properties":[{"id":"color","value":{"fixedColor":"#967302","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"recv_errs_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"recv_errs_lo"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"trans_bytes_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"trans_bytes_lo"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"trans_drop_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"trans_drop_lo"},"properties":[{"id":"color","value":{"fixedColor":"#967302","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"trans_errs_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"trans_errs_lo"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":7,"w":12,"x":0,"y":13},"id":74,"links":[],"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8","format":"time_series","intervalFactor":1,"legendFormat":"recv {{device}}","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8","format":"time_series","intervalFactor":1,"legendFormat":"trans {{device}} ","refId":"B","step":240}],"title":"Network Traffic Basic","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Disk space used of all filesystems mounted","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":7,"w":12,"x":12,"y":13},"id":152,"links":[],"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"100 - ((node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} * 100) / node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'})","format":"time_series","intervalFactor":1,"legendFormat":"{{mountpoint}}","refId":"A","step":240}],"title":"Disk Space Used Basic","type":"timeseries"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":20},"id":265,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"percentage","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":70,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"percent"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"percentunit"},"overrides":[{"matcher":{"id":"byName","options":"Idle - Waiting for something to happen"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Iowait - Waiting for I/O to complete"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Irq - Servicing interrupts"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Nice - Niced processes executing in user mode"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Softirq - Servicing softirqs"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Steal - Time spent in other operating systems when running in a virtualized environment"},"properties":[{"id":"color","value":{"fixedColor":"#FCE2DE","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"System - Processes executing in kernel mode"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"User - Normal processes executing in user mode"},"properties":[{"id":"color","value":{"fixedColor":"#5195CE","mode":"fixed"}}]}]},"gridPos":{"h":12,"w":12,"x":0,"y":7},"id":3,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":250},"tooltip":{"mode":"multi","sort":"desc"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"System - Processes executing in kernel mode","range":true,"refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"User - Normal processes executing in user mode","range":true,"refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"nice\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Nice - Niced processes executing in user mode","range":true,"refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Iowait - Waiting for I/O to complete","range":true,"refId":"E","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"irq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Irq - Servicing interrupts","range":true,"refId":"F","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"softirq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Softirq - Servicing softirqs","range":true,"refId":"G","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"steal\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","intervalFactor":1,"legendFormat":"Steal - Time spent in other operating systems when running in a virtualized environment","range":true,"refId":"H","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])))","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Idle - Waiting for something to happen","range":true,"refId":"J","step":240}],"title":"CPU","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap - Swap memory usage"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused - Free memory unassigned"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*Hardware Corrupted - *./"},"properties":[{"id":"custom.stacking","value":{"group":false,"mode":"normal"}}]}]},"gridPos":{"h":12,"w":12,"x":12,"y":7},"id":24,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"} - node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Apps - Memory used by user-space applications","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"PageTables - Memory used to map between virtual and physical memory addresses","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"SwapCache - Memory that keeps track of pages that have been fetched from swap but not yet been modified","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Slab - Memory used by the kernel to cache data structures for its own use (caches like inode, dentry, etc)","refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Cache - Parked file data (file content) cache","refId":"E","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Buffers - Block device (e.g. harddisk) cache","refId":"F","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Unused - Free memory unassigned","refId":"G","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Swap - Swap space used","refId":"H","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_HardwareCorrupted_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working","refId":"I","step":240}],"title":"Memory Stack","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bits out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bps"},"overrides":[{"matcher":{"id":"byName","options":"receive_packets_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"receive_packets_lo"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"transmit_packets_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"transmit_packets_lo"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":12,"w":12,"x":0,"y":19},"id":84,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Receive","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Transmit","refId":"B","step":240}],"title":"Network Traffic","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":12,"w":12,"x":12,"y":19},"id":156,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} - node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","intervalFactor":1,"legendFormat":"{{mountpoint}}","refId":"A","step":240}],"title":"Disk Space Used","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"IO read (-) / write (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"iops"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Read.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":12,"w":12,"x":0,"y":31},"id":229,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])","intervalFactor":4,"legendFormat":"{{device}} - Reads completed","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])","intervalFactor":1,"legendFormat":"{{device}} - Writes completed","refId":"B","step":240}],"title":"Disk IOps","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes read (-) / write (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"Bps"},"overrides":[{"matcher":{"id":"byName","options":"io time"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*read*./"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*sda.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byType","options":"time"},"properties":[{"id":"custom.axisPlacement","value":"hidden"}]}]},"gridPos":{"h":12,"w":12,"x":12,"y":31},"id":42,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"{{device}} - Successfully read bytes","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"{{device}} - Successfully written bytes","refId":"B","step":240}],"title":"I/O Usage Read / Write","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"%util","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":40,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"percentunit"},"overrides":[{"matcher":{"id":"byName","options":"io time"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]},{"matcher":{"id":"byType","options":"time"},"properties":[{"id":"custom.axisPlacement","value":"hidden"}]}]},"gridPos":{"h":12,"w":12,"x":0,"y":43},"id":127,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"} [$__rate_interval])","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"{{device}}","refId":"A","step":240}],"title":"I/O Utilization","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"percentage","axisPlacement":"auto","barAlignment":0,"drawStyle":"bars","fillOpacity":70,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"smooth","lineWidth":2,"pointSize":3,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"max":1,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"percentunit"},"overrides":[{"matcher":{"id":"byRegexp","options":"/^Guest - /"},"properties":[{"id":"color","value":{"fixedColor":"#5195ce","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/^GuestNice - /"},"properties":[{"id":"color","value":{"fixedColor":"#c15c17","mode":"fixed"}}]}]},"gridPos":{"h":12,"w":12,"x":12,"y":43},"id":319,"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[1m])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[1m])))","hide":false,"legendFormat":"Guest - Time spent running a virtual CPU for a guest operating system","range":true,"refId":"A"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\", mode=\"nice\"}[1m])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[1m])))","hide":false,"legendFormat":"GuestNice - Time spent running a niced guest (virtual CPU for guest operating system)","range":true,"refId":"B"}],"title":"CPU spent seconds in guests (VMs)","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"CPU / Memory / Net / Disk","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":21},"id":266,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":38},"id":136,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Inactive_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Inactive - Memory which has been less recently used. It is more eligible to be reclaimed for other purposes","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Active_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Active - Memory that has been used more recently and usually not reclaimed unless absolutely necessary","refId":"B","step":240}],"title":"Memory Active / Inactive","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*CommitLimit - *./"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":38},"id":135,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Committed_AS_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Committed_AS - Amount of memory presently allocated on the system","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_CommitLimit_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"CommitLimit - Amount of memory currently available to be allocated on the system","refId":"B","step":240}],"title":"Memory Committed","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":48},"id":191,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Inactive_file_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Inactive_file - File-backed memory on inactive LRU list","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Inactive_anon_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Inactive_anon - Anonymous and swap cache on inactive LRU list, including tmpfs (shmem)","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Active_file_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Active_file - File-backed memory on active LRU list","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Active_anon_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Active_anon - Anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs","refId":"D","step":240}],"title":"Memory Active / Inactive Detail","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total Swap"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":48},"id":130,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Writeback_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Writeback - Memory which is actively being written back to disk","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_WritebackTmp_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"WritebackTmp - Memory used by FUSE for temporary writeback buffers","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Dirty_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Dirty - Memory which is waiting to get written back to the disk","refId":"C","step":240}],"title":"Memory Writeback and Dirty","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages"},"properties":[{"id":"custom.fillOpacity","value":0}]},{"matcher":{"id":"byName","options":"ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages"},"properties":[{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":58},"id":138,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Mapped_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Mapped - Used memory in mapped pages files which have been mapped, such as libraries","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Shmem_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Shmem - Used shared memory (shared between several processes, thus including RAM disks)","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_ShmemHugePages_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_ShmemPmdMapped_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"ShmemPmdMapped - Amount of shared (shmem/tmpfs) memory backed by huge pages","refId":"D","step":240}],"title":"Memory Shared and Mapped","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total Swap"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":58},"id":131,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_SUnreclaim_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"SUnreclaim - Part of Slab, that cannot be reclaimed on memory pressure","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"SReclaimable - Part of Slab, that might be reclaimed, such as caches","refId":"B","step":240}],"title":"Memory Slab","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":68},"id":70,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_VmallocChunk_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"VmallocChunk - Largest contiguous block of vmalloc area which is free","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_VmallocTotal_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"VmallocTotal - Total size of vmalloc memory area","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_VmallocUsed_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"VmallocUsed - Amount of vmalloc area which is used","refId":"C","step":240}],"title":"Memory Vmalloc","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":68},"id":159,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Bounce_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Bounce - Memory used for block device bounce buffers","refId":"A","step":240}],"title":"Memory Bounce","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*Inactive *./"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":78},"id":129,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_AnonHugePages_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"AnonHugePages - Memory in anonymous huge pages","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_AnonPages_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"AnonPages - Memory in user pages not backed by files","refId":"B","step":240}],"title":"Memory Anonymous","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":78},"id":160,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_KernelStack_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"KernelStack - Kernel memory stack. This is not reclaimable","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Percpu_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"PerCPU - Per CPU memory allocated dynamically by loadable modules","refId":"B","step":240}],"title":"Memory Kernel / CPU","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"pages","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":88},"id":140,"links":[],"options":{"legend":{"calcs":["lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_HugePages_Free{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"HugePages_Free - Huge pages in the pool that are not yet allocated","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_HugePages_Rsvd{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_HugePages_Surp{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"HugePages_Surp - Huge pages in the pool above the value in /proc/sys/vm/nr_hugepages","refId":"C","step":240}],"title":"Memory HugePages Counter","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":88},"id":71,"links":[],"options":{"legend":{"calcs":["lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_HugePages_Total{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"HugePages - Total size of the pool of huge pages","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Hugepagesize - Huge Page size","refId":"B","step":240}],"title":"Memory HugePages Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":98},"id":128,"links":[],"options":{"legend":{"calcs":["mean","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_DirectMap1G_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"DirectMap1G - Amount of pages mapped as this size","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_DirectMap2M_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"DirectMap2M - Amount of pages mapped as this size","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_DirectMap4k_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"DirectMap4K - Amount of pages mapped as this size","refId":"C","step":240}],"title":"Memory DirectMap","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":98},"id":137,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Unevictable_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Unevictable - Amount of unevictable memory that can't be swapped out for a variety of reasons","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_Mlocked_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"MLocked - Size of pages locked to memory using the mlock() system call","refId":"B","step":240}],"title":"Memory Unevictable and MLocked","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total Swap"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":108},"id":132,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_memory_NFS_Unstable_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"NFS Unstable - Memory in NFS pages sent to the server, but not yet committed to the storage","refId":"A","step":240}],"title":"Memory NFS","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Memory Meminfo","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":22},"id":267,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"pages out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*out/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":25},"id":176,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pgpgin{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pagesin - Page in operations","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pgpgout{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pagesout - Page out operations","refId":"B","step":240}],"title":"Memory Pages In / Out","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"pages out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*out/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":25},"id":22,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pswpin{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pswpin - Pages swapped in","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pswpout{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pswpout - Pages swapped out","refId":"B","step":240}],"title":"Memory Pages Swap In / Out","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"faults","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"Apps"},"properties":[{"id":"color","value":{"fixedColor":"#629E51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#0A437C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working"},"properties":[{"id":"color","value":{"fixedColor":"#CFFAFF","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"RAM_Free"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab"},"properties":[{"id":"color","value":{"fixedColor":"#806EB7","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Free"},"properties":[{"id":"color","value":{"fixedColor":"#2F575E","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Unused"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Pgfault - Page major and minor fault operations"},"properties":[{"id":"custom.fillOpacity","value":0},{"id":"custom.stacking","value":{"group":false,"mode":"normal"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":35},"id":175,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":350},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pgfault - Page major and minor fault operations","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pgmajfault - Major page fault operations","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval]) - irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Pgminfault - Minor page fault operations","refId":"C","step":240}],"title":"Memory Page Faults","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#99440A","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Buffers"},"properties":[{"id":"color","value":{"fixedColor":"#58140C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cache"},"properties":[{"id":"color","value":{"fixedColor":"#6D1F62","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Cached"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Committed"},"properties":[{"id":"color","value":{"fixedColor":"#508642","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Dirty"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Free"},"properties":[{"id":"color","value":{"fixedColor":"#B7DBAB","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Mapped"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"PageTables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Page_Tables"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Slab_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Swap_Cache"},"properties":[{"id":"color","value":{"fixedColor":"#C15C17","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total"},"properties":[{"id":"color","value":{"fixedColor":"#511749","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total RAM + Swap"},"properties":[{"id":"color","value":{"fixedColor":"#052B51","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Total Swap"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"VmallocUsed"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":35},"id":307,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_vmstat_oom_kill{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"oom killer invocations ","refId":"A","step":240}],"title":"OOM Killer","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Memory Vmstat","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":23},"id":293,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"seconds","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"s"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Variation*./"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":40},"id":260,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_estimated_error_seconds{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Estimated error in seconds","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_offset_seconds{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Time offset in between local system and reference clock","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_maxerror_seconds{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Maximum error in seconds","refId":"C","step":240}],"title":"Time Synchronized Drift","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":40},"id":291,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_loop_time_constant{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Phase-locked loop time adjust","refId":"A","step":240}],"title":"Time PLL Adjust","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Variation*./"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":50},"id":168,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_sync_status{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Is clock synchronized to a reliable server (1 = yes, 0 = no)","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_frequency_adjustment_ratio{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Local clock frequency adjustment","refId":"B","step":240}],"title":"Time Synchronized Status","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"seconds","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":50},"id":294,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_tick_seconds{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Seconds between clock ticks","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_timex_tai_offset_seconds{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"International Atomic Time (TAI) offset","refId":"B","step":240}],"title":"Time Misc","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"System Timesync","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":24},"id":312,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":27},"id":62,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_procs_blocked{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Processes blocked waiting for I/O to complete","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_procs_running{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Processes in runnable state","refId":"B","step":240}],"title":"Processes Status","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":27},"id":315,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_processes_state{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{ state }}","refId":"A","step":240}],"title":"Processes State","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"forks / sec","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":37},"id":148,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_forks_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Processes forks second","refId":"A","step":240}],"title":"Processes Forks","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"decbytes"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Max.*/"},"properties":[{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":37},"id":149,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Processes virtual memory size in bytes","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"process_resident_memory_max_bytes{instance=\"$node\",job=\"$job\"}","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Maximum amount of virtual memory available in bytes","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Processes virtual memory size in bytes","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(process_virtual_memory_max_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Maximum amount of virtual memory available in bytes","refId":"D","step":240}],"title":"Processes Memory","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"PIDs limit"},"properties":[{"id":"color","value":{"fixedColor":"#F2495C","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":47},"id":313,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_processes_pids{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Number of PIDs","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_processes_max_processes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"PIDs limit","refId":"B","step":240}],"title":"PIDs Number and Limit","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"seconds","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"s"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*waiting.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":47},"id":305,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_schedstat_running_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"CPU {{ cpu }} - seconds spent running a process","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_schedstat_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"CPU {{ cpu }} - seconds spent by processing waiting for this CPU","refId":"B","step":240}],"title":"Process schedule stats Running / Waiting","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"Threads limit"},"properties":[{"id":"color","value":{"fixedColor":"#F2495C","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":57},"id":314,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_processes_threads{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Allocated threads","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_processes_max_threads{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Threads limit","refId":"B","step":240}],"title":"Threads Number and Limit","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"System Processes","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":25},"id":269,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":42},"id":8,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_context_switches_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"Context switches","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_intr_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"Interrupts","refId":"B","step":240}],"title":"Context Switches / Interrupts","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":42},"id":7,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_load1{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":4,"legendFormat":"Load 1m","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_load5{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":4,"legendFormat":"Load 5m","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_load15{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":4,"legendFormat":"Load 15m","refId":"C","step":240}],"title":"System Load","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Critical*./"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]},{"matcher":{"id":"byRegexp","options":"/.*Max*./"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":52},"id":259,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_interrupts_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{ type }} - {{ info }}","refId":"A","step":240}],"title":"Interrupts Detail","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":52},"id":306,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_schedstat_timeslices_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"CPU {{ cpu }}","refId":"A","step":240}],"title":"Schedule timeslices executed by each cpu","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":62},"id":151,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_entropy_available_bits{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Entropy available to random number generators","refId":"A","step":240}],"title":"Entropy","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"seconds","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":62},"id":308,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(process_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Time spent","refId":"A","step":240}],"title":"CPU time spent in user and system contexts","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Max*./"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":72},"id":64,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"process_max_fds{instance=\"$node\",job=\"$job\"}","interval":"","intervalFactor":1,"legendFormat":"Maximum open file descriptors","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"process_open_fds{instance=\"$node\",job=\"$job\"}","interval":"","intervalFactor":1,"legendFormat":"Open file descriptors","refId":"B","step":240}],"title":"File Descriptors","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"System Misc","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":26},"id":304,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"temperature","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"celsius"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Critical*./"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]},{"matcher":{"id":"byRegexp","options":"/.*Max*./"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":43},"id":158,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_hwmon_temp_celsius{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{ chip }} {{ sensor }} temp","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_hwmon_temp_crit_alarm_celsius{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":true,"interval":"","intervalFactor":1,"legendFormat":"{{ chip }} {{ sensor }} Critical Alarm","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_hwmon_temp_crit_celsius{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{ chip }} {{ sensor }} Critical","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_hwmon_temp_crit_hyst_celsius{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":true,"interval":"","intervalFactor":1,"legendFormat":"{{ chip }} {{ sensor }} Critical Historical","refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_hwmon_temp_max_celsius{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":true,"interval":"","intervalFactor":1,"legendFormat":"{{ chip }} {{ sensor }} Max","refId":"E","step":240}],"title":"Hardware temperature monitor","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Max*./"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":43},"id":300,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_cooling_device_cur_state{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"Current {{ name }} in {{ type }}","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_cooling_device_max_state{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Max {{ name }} in {{ type }}","refId":"B","step":240}],"title":"Throttle cooling device","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":53},"id":302,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_power_supply_online{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"{{ power_supply }} online","refId":"A","step":240}],"title":"Power supply","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Hardware Misc","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":27},"id":296,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":30},"id":297,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_systemd_socket_accepted_connections_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{ name }} Connections","refId":"A","step":240}],"title":"Systemd Sockets","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"Failed"},"properties":[{"id":"color","value":{"fixedColor":"#F2495C","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Inactive"},"properties":[{"id":"color","value":{"fixedColor":"#FF9830","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Active"},"properties":[{"id":"color","value":{"fixedColor":"#73BF69","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Deactivating"},"properties":[{"id":"color","value":{"fixedColor":"#FFCB7D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"Activating"},"properties":[{"id":"color","value":{"fixedColor":"#C8F2C2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":30},"id":298,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_systemd_units{instance=\"$node\",job=\"$job\",state=\"activating\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Activating","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_systemd_units{instance=\"$node\",job=\"$job\",state=\"active\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Active","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_systemd_units{instance=\"$node\",job=\"$job\",state=\"deactivating\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Deactivating","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_systemd_units{instance=\"$node\",job=\"$job\",state=\"failed\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Failed","refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_systemd_units{instance=\"$node\",job=\"$job\",state=\"inactive\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Inactive","refId":"E","step":240}],"title":"Systemd Units State","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Systemd","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":28},"id":270,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"The number (after merges) of I/O requests completed per second for the device","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"IO read (-) / write (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"iops"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Read.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":31},"id":9,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","intervalFactor":4,"legendFormat":"{{device}} - Reads completed","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","intervalFactor":1,"legendFormat":"{{device}} - Writes completed","refId":"B","step":240}],"title":"Disk IOps Completed","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"The number of bytes read from or written to the device per second","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes read (-) / write (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"Bps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Read.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":31},"id":33,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":4,"legendFormat":"{{device}} - Read bytes","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Written bytes","refId":"B","step":240}],"title":"Disk R/W Data","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"The average time for requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"time. read (-) / write (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":30,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"s"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Read.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":41},"id":37,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_read_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","hide":false,"interval":"","intervalFactor":4,"legendFormat":"{{device}} - Read wait time avg","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_write_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","hide":false,"interval":"","intervalFactor":1,"legendFormat":"{{device}} - Write wait time avg","refId":"B","step":240}],"title":"Disk Average Wait Time","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"The average queue length of the requests that were issued to the device","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"aqu-sz","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"none"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":41},"id":35,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_io_time_weighted_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","intervalFactor":4,"legendFormat":"{{device}}","refId":"A","step":240}],"title":"Average Queue Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"The number of read and write requests merged per second that were queued to the device","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"I/Os","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"iops"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Read.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":51},"id":133,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_reads_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","intervalFactor":1,"legendFormat":"{{device}} - Read merged","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_writes_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","intervalFactor":1,"legendFormat":"{{device}} - Write merged","refId":"B","step":240}],"title":"Disk R/W Merged","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Percentage of elapsed time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100% for devices serving requests serially. But for devices serving requests in parallel, such as RAID arrays and modern SSDs, this number does not reflect their performance limits.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"%util","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":30,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"percentunit"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":51},"id":36,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","intervalFactor":4,"legendFormat":"{{device}} - IO","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_discard_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","intervalFactor":4,"legendFormat":"{{device}} - discard","refId":"B","step":240}],"title":"Time Spent Doing I/Os","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"The number of outstanding requests at the instant the sample was taken. Incremented as requests are given to appropriate struct request_queue and decremented as they finish.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"Outstanding req.","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"none"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":61},"id":34,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_disk_io_now{instance=\"$node\",job=\"$job\"}","interval":"","intervalFactor":4,"legendFormat":"{{device}} - IO now","refId":"A","step":240}],"title":"Instantaneous Queue Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"IOs","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"iops"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*sda_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EAB839","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#6ED0E0","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EF843C","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#584477","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda2_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BA43A9","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sda3_.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F4D598","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#0A50A1","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#BF1B00","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdb3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0752D","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#962D82","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#614D93","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdc3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#9AC48A","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#65C5DB","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9934E","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#EA6460","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde1.*/"},"properties":[{"id":"color","value":{"fixedColor":"#E0F9D7","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sdd2.*/"},"properties":[{"id":"color","value":{"fixedColor":"#FCEACA","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*sde3.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F9E2D2","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":61},"id":301,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_discards_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","intervalFactor":4,"legendFormat":"{{device}} - Discards completed","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_disk_discards_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","intervalFactor":1,"legendFormat":"{{device}} - Discards merged","refId":"B","step":240}],"title":"Disk IOps Discards completed / merged","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Storage Disk","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":29},"id":271,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":46},"id":43,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"{{mountpoint}} - Available","metric":"","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_free_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","hide":true,"intervalFactor":1,"legendFormat":"{{mountpoint}} - Free","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","hide":true,"intervalFactor":1,"legendFormat":"{{mountpoint}} - Size","refId":"C","step":240}],"title":"Filesystem space available","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"file nodes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":46},"id":41,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_files_free{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"{{mountpoint}} - Free file nodes","refId":"A","step":240}],"title":"File Nodes Free","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"files","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":56},"id":28,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filefd_maximum{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":4,"legendFormat":"Max open files","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filefd_allocated{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"Open files","refId":"B","step":240}],"title":"File Descriptor","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"file Nodes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":56},"id":219,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_files{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"{{mountpoint}} - File nodes total","refId":"A","step":240}],"title":"File Nodes Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"/ ReadOnly"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":66},"id":44,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_readonly{instance=\"$node\",job=\"$job\",device!~'rootfs'}","format":"time_series","intervalFactor":1,"legendFormat":"{{mountpoint}} - ReadOnly","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_filesystem_device_error{instance=\"$node\",job=\"$job\",device!~'rootfs',fstype!~'tmpfs'}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{mountpoint}} - Device error","refId":"B","step":240}],"title":"Filesystem in ReadOnly / Error","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Storage Filesystem","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":30},"id":272,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byName","options":"receive_packets_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"receive_packets_lo"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"transmit_packets_eth0"},"properties":[{"id":"color","value":{"fixedColor":"#7EB26D","mode":"fixed"}}]},{"matcher":{"id":"byName","options":"transmit_packets_lo"},"properties":[{"id":"color","value":{"fixedColor":"#E24D42","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":31},"id":60,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{device}} - Receive","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"{{device}} - Transmit","refId":"B","step":240}],"title":"Network Traffic by Packets","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":31},"id":142,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Receive errors","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Rransmit errors","refId":"B","step":240}],"title":"Network Traffic Errors","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":41},"id":143,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Receive drop","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Transmit drop","refId":"B","step":240}],"title":"Network Traffic Drop","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":41},"id":141,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Receive compressed","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Transmit compressed","refId":"B","step":240}],"title":"Network Traffic Compressed","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":51},"id":146,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_multicast_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Receive multicast","refId":"A","step":240}],"title":"Network Traffic Multicast","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":51},"id":144,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Receive fifo","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Transmit fifo","refId":"B","step":240}],"title":"Network Traffic Fifo","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"pps"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":61},"id":145,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_receive_frame_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"intervalFactor":1,"legendFormat":"{{device}} - Receive frame","refId":"A","step":240}],"title":"Network Traffic Frame","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":61},"id":231,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_carrier_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Statistic transmit_carrier","refId":"A","step":240}],"title":"Network Traffic Carrier","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Trans.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":71},"id":232,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_network_transmit_colls_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"{{device}} - Transmit colls","refId":"A","step":240}],"title":"Network Traffic Colls","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"entries","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byName","options":"NF conntrack limit"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":71},"id":61,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_nf_conntrack_entries{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"NF conntrack entries","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_nf_conntrack_entries_limit{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"NF conntrack limit","refId":"B","step":240}],"title":"NF Contrack","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"Entries","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":81},"id":230,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_arp_entries{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"{{ device }} - ARP entries","refId":"A","step":240}],"title":"ARP Entries","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"decimals":0,"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":81},"id":288,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_network_mtu_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"{{ device }} - Bytes","refId":"A","step":240}],"title":"MTU","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"decimals":0,"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":91},"id":280,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_network_speed_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"{{ device }} - Speed","refId":"A","step":240}],"title":"Speed","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packets","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"decimals":0,"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"none"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":91},"id":289,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_network_transmit_queue_length{instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"{{ device }} - Interface transmit queue length","refId":"A","step":240}],"title":"Queue Length","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"packetes drop (-) / process (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Dropped.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":101},"id":290,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_softnet_processed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"CPU {{cpu}} - Processed","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_softnet_dropped_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"CPU {{cpu}} - Dropped","refId":"B","step":240}],"title":"Softnet Packets","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":101},"id":310,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_softnet_times_squeezed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"CPU {{cpu}} - Squeezed","refId":"A","step":240}],"title":"Softnet Out of Quota","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":111},"id":309,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_network_up{operstate=\"up\",instance=\"$node\",job=\"$job\"}","format":"time_series","intervalFactor":1,"legendFormat":"{{interface}} - Operational state UP","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_network_carrier{instance=\"$node\",job=\"$job\"}","format":"time_series","instant":false,"legendFormat":"{{device}} - Physical link state","refId":"B"}],"title":"Network Operational Status","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Network Traffic","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":31},"id":273,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":32},"id":63,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_TCP_alloc{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"TCP_alloc - Allocated sockets","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_TCP_inuse{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"TCP_inuse - Tcp sockets currently in use","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_TCP_mem{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":true,"interval":"","intervalFactor":1,"legendFormat":"TCP_mem - Used memory for tcp","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_TCP_orphan{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"TCP_orphan - Orphan sockets","refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_TCP_tw{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"TCP_tw - Sockets waiting close","refId":"E","step":240}],"title":"Sockstat TCP","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":32},"id":124,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_UDPLITE_inuse{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"UDPLITE_inuse - Udplite sockets currently in use","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_UDP_inuse{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"UDP_inuse - Udp sockets currently in use","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_UDP_mem{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"UDP_mem - Used memory for udp","refId":"C","step":240}],"title":"Sockstat UDP","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":42},"id":125,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_FRAG_inuse{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"FRAG_inuse - Frag sockets currently in use","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_RAW_inuse{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"RAW_inuse - Raw sockets currently in use","refId":"C","step":240}],"title":"Sockstat FRAG / RAW","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"bytes","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"bytes"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":42},"id":220,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_TCP_mem_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"mem_bytes - TCP sockets in that state","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_UDP_mem_bytes{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"mem_bytes - UDP sockets in that state","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_FRAG_memory{instance=\"$node\",job=\"$job\"}","interval":"","intervalFactor":1,"legendFormat":"FRAG_memory - Used memory for frag","refId":"C"}],"title":"Sockstat Memory Size","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"sockets","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":52},"id":126,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_sockstat_sockets_used{instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Sockets_used - Sockets currently in use","refId":"A","step":240}],"title":"Sockstat Used","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Network Sockstat","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":32},"id":274,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"octets out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Out.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":33},"id":221,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_IpExt_InOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"InOctets - Received octets","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_IpExt_OutOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","intervalFactor":1,"legendFormat":"OutOctets - Sent octets","refId":"B","step":240}],"title":"Netstat IP In / Out Octets","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"datagrams","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":33},"id":81,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true,"width":300},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Ip_Forwarding{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"Forwarding - IP forwarding","refId":"A","step":240}],"title":"Netstat IP Forwarding","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"messages out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Out.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":43},"id":115,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Icmp_InMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"InMsgs - Messages which the entity received. Note that this counter includes all those counted by icmpInErrors","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Icmp_OutMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"OutMsgs - Messages which this entity attempted to send. Note that this counter includes all those counted by icmpOutErrors","refId":"B","step":240}],"title":"ICMP In / Out","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"messages out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Out.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":43},"id":50,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Icmp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"InErrors - Messages which the entity received but determined as having ICMP-specific errors (bad ICMP checksums, bad length, etc.)","refId":"A","step":240}],"title":"ICMP Errors","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"datagrams out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Out.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*Snd.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":53},"id":55,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Udp_InDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"InDatagrams - Datagrams received","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Udp_OutDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"OutDatagrams - Datagrams sent","refId":"B","step":240}],"title":"UDP In / Out","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"datagrams","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":53},"id":109,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Udp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"InErrors - UDP Datagrams that could not be delivered to an application","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Udp_NoPorts{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"NoPorts - UDP Datagrams received on a port with no listener","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_UdpLite_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","legendFormat":"InErrors Lite - UDPLite Datagrams that could not be delivered to an application","refId":"C"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Udp_RcvbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"RcvbufErrors - UDP buffer errors received","refId":"D","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Udp_SndbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"SndbufErrors - UDP buffer errors send","refId":"E","step":240}],"title":"UDP Errors","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"datagrams out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Out.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]},{"matcher":{"id":"byRegexp","options":"/.*Snd.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":63},"id":299,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_InSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","instant":false,"interval":"","intervalFactor":1,"legendFormat":"InSegs - Segments received, including those received in error. This count includes segments received on currently established connections","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_OutSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"OutSegs - Segments sent, including those on current connections but excluding those containing only retransmitted octets","refId":"B","step":240}],"title":"TCP In / Out","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":63},"id":104,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_TcpExt_ListenOverflows{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"ListenOverflows - Times the listen queue of a socket overflowed","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_TcpExt_ListenDrops{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"ListenDrops - SYNs to LISTEN sockets ignored","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_TcpExt_TCPSynRetrans{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"TCPSynRetrans - SYN-SYN/ACK retransmits to break down retransmissions in SYN, fast/timeout retransmits","refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_RetransSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","legendFormat":"RetransSegs - Segments retransmitted - that is, the number of TCP segments transmitted containing one or more previously transmitted octets","refId":"D"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_InErrs{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","legendFormat":"InErrs - Segments received in error (e.g., bad TCP checksums)","refId":"E"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_OutRsts{instance=\"$node\",job=\"$job\"}[$__rate_interval])","interval":"","legendFormat":"OutRsts - Segments sent with RST flag","refId":"F"}],"title":"TCP Errors","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"connections","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*MaxConn *./"},"properties":[{"id":"color","value":{"fixedColor":"#890F02","mode":"fixed"}},{"id":"custom.fillOpacity","value":0}]}]},"gridPos":{"h":10,"w":12,"x":0,"y":73},"id":85,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_netstat_Tcp_CurrEstab{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"CurrEstab - TCP connections for which the current state is either ESTABLISHED or CLOSE- WAIT","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_netstat_Tcp_MaxConn{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"MaxConn - Limit on the total number of TCP connections the entity can support (Dynamic is \"-1\")","refId":"B","step":240}],"title":"TCP Connections","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter out (-) / in (+)","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*Sent.*/"},"properties":[{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":73},"id":91,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_TcpExt_SyncookiesFailed{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"SyncookiesFailed - Invalid SYN cookies received","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_TcpExt_SyncookiesRecv{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"SyncookiesRecv - SYN cookies received","refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_TcpExt_SyncookiesSent{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"SyncookiesSent - SYN cookies sent","refId":"C","step":240}],"title":"TCP SynCookie","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"connections","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":83},"id":82,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_ActiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"ActiveOpens - TCP connections that have made a direct transition to the SYN-SENT state from the CLOSED state","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"irate(node_netstat_Tcp_PassiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"PassiveOpens - TCP connections that have made a direct transition to the SYN-RCVD state from the LISTEN state","refId":"B","step":240}],"title":"TCP Direct Transition","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"Enable with --collector.tcpstat argument on node-exporter","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"connections","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green"}]},"unit":"short"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":12,"y":83},"id":320,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"node_tcp_connection_states{state=\"established\",instance=\"$node\",job=\"$job\"}","format":"time_series","interval":"","intervalFactor":1,"legendFormat":"established - TCP sockets in established state","range":true,"refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"node_tcp_connection_states{state=\"fin_wait2\",instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"fin_wait2 - TCP sockets in fin_wait2 state","range":true,"refId":"B","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"node_tcp_connection_states{state=\"listen\",instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"listen - TCP sockets in listen state","range":true,"refId":"C","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"editorMode":"code","expr":"node_tcp_connection_states{state=\"time_wait\",instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"time_wait - TCP sockets in time_wait state","range":true,"refId":"D","step":240}],"title":"TCP Stat","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Network Netstat","type":"row"},{"collapsed":true,"datasource":{"type":"prometheus","uid":"000000001"},"gridPos":{"h":1,"w":24,"x":0,"y":33},"id":279,"panels":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"seconds","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"s"},"overrides":[]},"gridPos":{"h":10,"w":12,"x":0,"y":50},"id":40,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_scrape_collector_duration_seconds{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"{{collector}} - Scrape duration","refId":"A","step":240}],"title":"Node Exporter Scrape Time","type":"timeseries"},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"description":"","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"counter","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":20,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"linear","lineStyle":{"fill":"solid"},"lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"links":[],"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"},{"color":"red","value":80}]},"unit":"short"},"overrides":[{"matcher":{"id":"byRegexp","options":"/.*error.*/"},"properties":[{"id":"color","value":{"fixedColor":"#F2495C","mode":"fixed"}},{"id":"custom.transform","value":"negative-Y"}]}]},"gridPos":{"h":10,"w":12,"x":12,"y":50},"id":157,"links":[],"options":{"legend":{"calcs":["mean","lastNotNull","max","min"],"displayMode":"table","placement":"bottom","showLegend":true},"tooltip":{"mode":"multi","sort":"none"}},"pluginVersion":"9.2.0","targets":[{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_scrape_collector_success{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"{{collector}} - Scrape success","refId":"A","step":240},{"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"expr":"node_textfile_scrape_error{instance=\"$node\",job=\"$job\"}","format":"time_series","hide":false,"interval":"","intervalFactor":1,"legendFormat":"{{collector}} - Scrape textfile error (1 = true)","refId":"B","step":240}],"title":"Node Exporter Scrape","type":"timeseries"}],"targets":[{"datasource":{"type":"prometheus","uid":"000000001"},"refId":"A"}],"title":"Node Exporter","type":"row"}],"refresh":"5s","revision":1,"schemaVersion":38,"style":"dark","tags":["linux"],"templating":{"list":[{"current":{"selected":false,"text":"default","value":"default"},"hide":0,"includeAll":false,"label":"datasource","multi":false,"name":"DS_PROMETHEUS","options":[],"query":"prometheus","refresh":1,"regex":"","skipUrlSync":false,"type":"datasource"},{"current":{"selected":false,"text":"node","value":"node"},"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"definition":"","hide":0,"includeAll":false,"label":"Job","multi":false,"name":"job","options":[],"query":{"query":"label_values(node_uname_info, job)","refId":"Prometheus-job-Variable-Query"},"refresh":1,"regex":"","skipUrlSync":false,"sort":1,"tagValuesQuery":"","tagsQuery":"","type":"query","useTags":false},{"current":{"selected":false,"text":"node-exporter:9100","value":"node-exporter:9100"},"datasource":{"type":"prometheus","uid":"P92AEBB27A9B79E22"},"definition":"label_values(node_uname_info{job=\"$job\"}, instance)","hide":0,"includeAll":false,"label":"Host","multi":false,"name":"node","options":[],"query":{"query":"label_values(node_uname_info{job=\"$job\"}, instance)","refId":"Prometheus-node-Variable-Query"},"refresh":1,"regex":"","skipUrlSync":false,"sort":1,"tagValuesQuery":"","tagsQuery":"","type":"query","useTags":false},{"current":{"selected":false,"text":"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+","value":"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+"},"hide":2,"includeAll":false,"multi":false,"name":"diskdevices","options":[{"selected":true,"text":"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+","value":"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+"}],"query":"[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+","skipUrlSync":false,"type":"custom"}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"browser","title":"Node Exporter Full","uid":"rYdddlPWk","version":2,"weekStart":""} diff --git a/etc/grafana/grafana.ini b/etc/grafana/grafana.ini new file mode 100644 index 00000000..8c64ef71 --- /dev/null +++ b/etc/grafana/grafana.ini @@ -0,0 +1,4 @@ +[auth.anonymous] +enabled = true +org_name = Main Org. +org_role = Admin \ No newline at end of file diff --git a/etc/grafana/provisioning/dashboards/foyer.yml b/etc/grafana/provisioning/dashboards/foyer.yml new file mode 100644 index 00000000..38812c65 --- /dev/null +++ b/etc/grafana/provisioning/dashboards/foyer.yml @@ -0,0 +1,15 @@ +apiVersion: 1 + +providers: + - name: "foyer provider" + disableDeletion: true + allowUiUpdates: false + options: + path: /var/lib/grafana/dashboards/foyer.json + foldersFromFilesStructure: false + - name: "node exporter full provider" + disableDeletion: true + allowUiUpdates: false + options: + path: /var/lib/grafana/dashboards/node-exporter-full.json + foldersFromFilesStructure: false \ No newline at end of file diff --git a/etc/grafana/provisioning/datasources/foyer.yml b/etc/grafana/provisioning/datasources/foyer.yml new file mode 100644 index 00000000..187f4855 --- /dev/null +++ b/etc/grafana/provisioning/datasources/foyer.yml @@ -0,0 +1,14 @@ +apiVersion: 1 +deleteDatasources: + - name: foyer +datasources: + - name: foyer + type: prometheus + access: proxy + url: http://prometheus:9090 + withCredentials: false + tlsAuth: false + tlsAuthWithCACert: false + version: 1 + editable: true + isDefault: true \ No newline at end of file diff --git a/etc/prometheus/prometheus.yml b/etc/prometheus/prometheus.yml new file mode 100644 index 00000000..5cfb861e --- /dev/null +++ b/etc/prometheus/prometheus.yml @@ -0,0 +1,16 @@ +global: + scrape_interval: 1s + evaluation_interval: 1s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'node' + static_configs: + - targets: ['node-exporter:9100'] + + - job_name: 'foyer-storage-bench' + static_configs: + - targets: ['host.docker.internal:19970'] diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 00000000..b7f82eb6 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "examples" +version = "0.0.0" +edition = "2021" +authors = ["MrCroxx "] +description = "Hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +publish = false + +[dependencies] +foyer = { version = "*", path = "../foyer" } + +[[example]] +name = "memory" +path = "memory.rs" diff --git a/examples/memory.rs b/examples/memory.rs new file mode 100644 index 00000000..69a76cb9 --- /dev/null +++ b/examples/memory.rs @@ -0,0 +1,35 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::hash::RandomState; + +use foyer::memory::{Cache, DefaultCacheEventListener, LruCacheConfig, LruConfig}; + +fn main() { + let cache = Cache::lru(LruCacheConfig { + capacity: 16, + shards: 4, + eviction_config: LruConfig { + high_priority_pool_ratio: 0.1, + }, + object_pool_capacity: 1024, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }); + + let entry = cache.insert("hello".to_string(), "world".to_string(), 1); + let e = cache.get("hello").unwrap(); + + assert_eq!(entry.value(), e.value()); +} diff --git a/foyer-common/Cargo.toml b/foyer-common/Cargo.toml new file mode 100644 index 00000000..335ae28e --- /dev/null +++ b/foyer-common/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "foyer-common" +version = "0.5.0" +edition = "2021" +authors = ["MrCroxx "] +description = "common utils for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +anyhow = "1.0" +bytes = "1" +cfg-if = "1" +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +itertools = "0.12" +parking_lot = { version = "0.12", features = ["arc_lock"] } +paste = "1.0" +tokio = { workspace = true } +tracing = "0.1" + +[dev-dependencies] +rand = "0.8.5" diff --git a/foyer-common/src/async_queue.rs b/foyer-common/src/async_queue.rs new file mode 100644 index 00000000..fbaba58f --- /dev/null +++ b/foyer-common/src/async_queue.rs @@ -0,0 +1,134 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{collections::VecDeque, fmt::Debug}; + +use parking_lot::Mutex; +use tokio::sync::{watch, Notify}; + +#[derive(Debug)] +pub struct AsyncQueue { + queue: Mutex>, + notified: Notify, + watch_tx: watch::Sender, + watch_rx: watch::Receiver, +} + +impl Default for AsyncQueue { + fn default() -> Self { + Self::new() + } +} + +impl AsyncQueue { + pub fn new() -> Self { + let (watch_tx, watch_rx) = watch::channel(0); + Self { + queue: Mutex::new(VecDeque::default()), + notified: Notify::new(), + watch_tx, + watch_rx, + } + } + + pub fn try_acquire(&self) -> Option { + let mut guard = self.queue.lock(); + if let Some(item) = guard.pop_front() { + if !guard.is_empty() { + // Since in `release` we use `notify_one`, not all waiters + // will be waken up. Therefore if we figure out that the queue is not empty, + // we call `notify_one` to awake the next pending `acquire`. + self.notified.notify_one(); + } + self.watch_tx.send(guard.len()).unwrap(); + Some(item) + } else { + None + } + } + + pub async fn acquire(&self) -> T { + loop { + let notified = self.notified.notified(); + { + let mut guard = self.queue.lock(); + if let Some(item) = guard.pop_front() { + if !guard.is_empty() { + // Since in `release` we use `notify_one`, not all waiters + // will be waken up. Therefore if we figure out that the queue is not empty, + // we call `notify_one` to awake the next pending `acquire`. + self.notified.notify_one(); + } + self.watch_tx.send(guard.len()).unwrap(); + break item; + } + } + notified.await; + } + } + + pub fn release(&self, item: T) { + let mut guard = self.queue.lock(); + guard.push_back(item); + self.watch_tx.send(guard.len()).unwrap(); + self.notified.notify_one(); + } + + pub fn len(&self) -> usize { + *self.watch_rx.borrow() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn watch(&self) -> watch::Receiver { + self.watch_rx.clone() + } + + pub fn flash(&self) { + self.watch_tx.send(self.len()).unwrap(); + } +} + +#[cfg(test)] +mod tests { + use std::{ + future::{poll_fn, Future}, + pin::pin, + task::{Poll, Poll::Pending}, + }; + + use crate::async_queue::AsyncQueue; + + #[tokio::test] + async fn test_basic() { + let queue = AsyncQueue::new(); + queue.release(1); + assert_eq!(1, queue.acquire().await); + } + + #[tokio::test] + async fn test_multiple_reader() { + let queue = AsyncQueue::new(); + let mut read_future1 = pin!(queue.acquire()); + let mut read_future2 = pin!(queue.acquire()); + assert_eq!(Pending, poll_fn(|cx| Poll::Ready(read_future1.as_mut().poll(cx))).await); + assert_eq!(Pending, poll_fn(|cx| Poll::Ready(read_future2.as_mut().poll(cx))).await); + queue.release(1); + queue.release(2); + assert_eq!(1, read_future1.await); + assert_eq!(2, read_future2.await); + } +} diff --git a/foyer-common/src/batch.rs b/foyer-common/src/batch.rs new file mode 100644 index 00000000..a9a72ea6 --- /dev/null +++ b/foyer-common/src/batch.rs @@ -0,0 +1,72 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use parking_lot::Mutex; +use tokio::sync::oneshot; + +const DEFAULT_CAPACITY: usize = 16; + +pub enum Identity { + Leader(oneshot::Receiver), + Follower(oneshot::Receiver), +} + +#[derive(Debug)] +pub struct Item { + pub arg: A, + pub tx: oneshot::Sender, +} + +#[derive(Debug)] +pub struct Batch { + queue: Mutex>>, +} + +impl Default for Batch { + fn default() -> Self { + Self::new() + } +} + +impl Batch { + pub fn new() -> Self { + Self::with_capacity(DEFAULT_CAPACITY) + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + queue: Mutex::new(Vec::with_capacity(capacity)), + } + } + + pub fn push(&self, arg: A) -> Identity { + let (tx, rx) = oneshot::channel(); + let item = Item { arg, tx }; + let mut queue = self.queue.lock(); + let is_leader = queue.is_empty(); + queue.push(item); + if is_leader { + Identity::Leader(rx) + } else { + Identity::Follower(rx) + } + } + + pub fn rotate(&self) -> Vec> { + let mut queue = self.queue.lock(); + let mut q = Vec::with_capacity(queue.capacity()); + std::mem::swap(&mut *queue, &mut q); + q + } +} diff --git a/foyer-common/src/bits.rs b/foyer-common/src/bits.rs new file mode 100644 index 00000000..245e02be --- /dev/null +++ b/foyer-common/src/bits.rs @@ -0,0 +1,141 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fmt::{Debug, Display}, + ops::{Add, BitAnd, Not, Sub}, +}; + +// TODO(MrCroxx): Use `trait_alias` after stable. +// pub trait UnsignedTrait = Add +// + Sub +// + BitAnd +// + Not +// + Sized +// + From +// + Eq +// + Debug +// + Display +// + Clone +// + Copy; + +pub trait Unsigned: + Add + + Sub + + BitAnd + + Not + + Sized + + From + + Eq + + Debug + + Display + + Clone + + Copy +{ +} + +impl< + U: Add + + Sub + + BitAnd + + Not + + Sized + + From + + Eq + + Debug + + Display + + Clone + + Copy, + > Unsigned for U +{ +} + +#[inline(always)] +pub fn is_pow2(v: U) -> bool { + v & (v - U::from(1)) == U::from(0) +} + +#[inline(always)] +pub fn assert_pow2(v: U) { + assert_eq!(v & (v - U::from(1)), U::from(0), "v: {}", v); +} + +#[inline(always)] +pub fn debug_assert_pow2(v: U) { + debug_assert_eq!(v & (v - U::from(1)), U::from(0), "v: {}", v); +} + +#[inline(always)] +pub fn is_aligned(align: U, v: U) -> bool { + debug_assert_pow2(align); + v & (align - U::from(1)) == U::from(0) +} + +#[inline(always)] +pub fn assert_aligned(align: U, v: U) { + debug_assert_pow2(align); + assert!(is_aligned(align, v), "align: {}, v: {}", align, v); +} + +#[inline(always)] +pub fn debug_assert_aligned(align: U, v: U) { + debug_assert_pow2(align); + debug_assert!(is_aligned(align, v), "align: {}, v: {}", align, v); +} + +#[inline(always)] +pub fn align_up(align: U, v: U) -> U { + debug_assert_pow2(align); + (v + align - U::from(1)) & !(align - U::from(1)) +} + +#[inline(always)] +pub fn align_down(align: U, v: U) -> U { + debug_assert_pow2(align); + v & !(align - U::from(1)) +} + +// macro_rules! bpf_buffer_trace { +// ($buf:expr, $span:expr) => { +// #[cfg(feature = "bpf")] +// { +// if $buf.len() >= 16 && let Some(id) = $span.id() { +// use bytes::BufMut; + +// const BPF_BUFFER_TRACE_MAGIC: u64 = 0xdeadbeefdeadbeef; + +// $span.record("sid", id.into_u64()); + +// (&mut $buf[0..8]).put_u64_le(BPF_BUFFER_TRACE_MAGIC); +// (&mut $buf[8..16]).put_u64_le(id.into_u64()); +// } +// } +// }; +// } + +// pub(crate) use bpf_buffer_trace; diff --git a/foyer-common/src/code.rs b/foyer-common/src/code.rs new file mode 100644 index 00000000..22d35d67 --- /dev/null +++ b/foyer-common/src/code.rs @@ -0,0 +1,495 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{marker::PhantomData, sync::Arc}; + +use bytes::{Buf, BufMut}; +use paste::paste; + +pub type CodingError = anyhow::Error; +pub type CodingResult = Result; + +pub trait BufExt: Buf { + // TODO(MrCroxx): Use `cfg_match` after stable. + // cfg_match! { + // cfg(target_pointer_width = "16") => { + // fn get_usize(&mut self) -> usize { + // self.get_u16() as usize + // } + + // fn get_isize(&mut self) -> isize { + // self.get_i16() as isize + // } + // } + // cfg(target_pointer_width = "32") => { + // fn get_usize(&mut self) -> usize { + // self.get_u32() as usize + // } + + // fn get_isize(&mut self) -> isize { + // self.get_i32() as isize + // } + // } + // cfg(target_pointer_width = "64") => { + // fn get_usize(&mut self) -> usize { + // self.get_u64() as usize + // } + + // fn get_isize(&mut self) -> isize { + // self.get_i64() as isize + // } + // } + // } + cfg_if::cfg_if! { + if #[cfg(target_pointer_width = "16")] { + fn get_usize(&mut self) -> usize { + self.get_u16() as usize + } + fn get_isize(&mut self) -> isize { + self.get_i16() as isize + } + } + else if #[cfg(target_pointer_width = "32")] { + fn get_usize(&mut self) -> usize { + self.get_u32() as usize + } + fn get_isize(&mut self) -> isize { + self.get_i32() as isize + } + } + else if #[cfg(target_pointer_width = "64")] { + fn get_usize(&mut self) -> usize { + self.get_u64() as usize + } + fn get_isize(&mut self) -> isize { + self.get_i64() as isize + } + } + } +} + +impl BufExt for T {} + +pub trait BufMutExt: BufMut { + // TODO(MrCroxx): Use `cfg_match` after stable. + // cfg_match! { + // cfg(target_pointer_width = "16") => { + // fn put_usize(&mut self, v: usize) { + // self.put_u16(v as u16); + // } + + // fn put_isize(&mut self, v: isize) { + // self.put_i16(v as i16); + // } + // } + // cfg(target_pointer_width = "32") => { + // fn put_usize(&mut self, v: usize) { + // self.put_u32(v as u32); + // } + + // fn put_isize(&mut self, v: isize) { + // self.put_i32(v as i32); + // } + // } + // cfg(target_pointer_width = "64") => { + // fn put_usize(&mut self, v: usize) { + // self.put_u64(v as u64); + // } + + // fn put_isize(&mut self, v: isize) { + // self.put_i64(v as i64); + // } + // } + // } + cfg_if::cfg_if! { + if #[cfg(target_pointer_width = "16")] { + fn put_usize(&mut self, v: usize) { + self.put_u16(v as u16); + } + fn put_isize(&mut self, v: isize) { + self.put_i16(v as i16); + } + } + else if #[cfg(target_pointer_width = "32")] { + fn put_usize(&mut self, v: usize) { + self.put_u32(v as u32); + } + fn put_isize(&mut self, v: isize) { + self.put_i32(v as i32); + } + } + else if #[cfg(target_pointer_width = "64")] { + fn put_usize(&mut self, v: usize) { + self.put_u64(v as u64); + } + fn put_isize(&mut self, v: isize) { + self.put_i64(v as i64); + } + } + } +} + +impl BufMutExt for T {} + +pub trait Cursor: Send + Sync + 'static + std::io::Read + std::fmt::Debug { + fn into_inner(self) -> T; +} + +/// [`Key`] is required to implement [`Clone`]. +/// +/// If cloning a [`Key`] is expensive, wrap it with [`Arc`]. +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[allow(unused_variables)] +pub trait Key: + Sized + Send + Sync + 'static + std::hash::Hash + Eq + PartialEq + Ord + PartialOrd + std::fmt::Debug + Clone +{ + // TODO(MrCroxx): Restore this after `associated_type_defaults` is stable. + // type Cursor: Cursor = UnimplementedCursor; + type Cursor: Cursor; + + /// memory weight + fn weight(&self) -> usize { + std::mem::size_of::() + } + + fn serialized_len(&self) -> usize { + panic!("Method `serialized_len` must be implemented for `Key` if storage is used.") + } + + fn read(buf: &[u8]) -> CodingResult { + panic!("Method `read` must be implemented for `Key` if storage is used.") + } + + fn into_cursor(self) -> Self::Cursor { + panic!("Associated type `Cursor` and method `into_cursor` must be implemented for `Key` if storage is used.") + } +} + +/// [`Value`] is required to implement [`Clone`]. +/// +/// If cloning a [`Value`] is expensive, wrap it with [`Arc`]. +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[allow(unused_variables)] +pub trait Value: Sized + Send + Sync + 'static + std::fmt::Debug + Clone { + // TODO(MrCroxx): Restore this after `associated_type_defaults` is stable. + // type Cursor: Cursor = UnimplementedCursor; + type Cursor: Cursor; + + /// memory weight + fn weight(&self) -> usize { + std::mem::size_of::() + } + + fn serialized_len(&self) -> usize { + panic!("Method `serialized_len` must be implemented for `Value` if storage is used.") + } + + fn read(buf: &[u8]) -> CodingResult { + panic!("Method `read` must be implemented for `Value` if storage is used.") + } + + fn into_cursor(self) -> Self::Cursor { + panic!("Associated type `Cursor` and method `into_cursor` must be implemented for `Value` if storage is used.") + } +} + +macro_rules! for_all_primitives { + ($macro:ident) => { + $macro! { + {u8, U8}, + {u16, U16}, + {u32, U32}, + {u64, U64}, + {usize, Usize}, + {i8, I8}, + {i16, I16}, + {i32, I32}, + {i64, I64}, + {isize, Isize}, + } + }; +} + +macro_rules! def_cursor { + ($( { $type:ty, $id:ident }, )*) => { + paste! { + $( + #[derive(Debug)] + pub struct [] { + inner: $type, + pos: u8, + } + + impl [] { + pub fn new(inner: $type) -> Self { + Self { + inner, + pos: 0, + } + } + } + + impl std::io::Read for [] { + fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { + let slice = self.inner.to_be_bytes(); + let len = std::cmp::min(slice.len() - self.pos as usize, buf.len()); + buf.put_slice(&slice[self.pos as usize..self.pos as usize + len]); + self.pos += len as u8; + Ok(len) + } + } + + impl Cursor<$type> for [] { + fn into_inner(self) -> $type { + self.inner + } + } + )* + } + }; +} + +macro_rules! impl_key { + ($( { $type:ty, $id:ident }, )*) => { + paste! { + $( + impl Key for $type { + type Cursor = []; + + fn serialized_len(&self) -> usize { + std::mem::size_of::<$type>() + } + + fn read(mut buf: &[u8]) -> CodingResult { + Ok(buf.[< get_ $type>]()) + } + + fn into_cursor(self) -> Self::Cursor { + []::new(self) + } + } + )* + } + }; +} + +macro_rules! impl_value { + ($( { $type:ty, $id:ident }, )*) => { + paste! { + $( + impl Value for $type { + type Cursor = []; + + fn serialized_len(&self) -> usize { + std::mem::size_of::<$type>() + } + + fn read(mut buf: &[u8]) -> CodingResult { + Ok(buf.[< get_ $type>]()) + } + + fn into_cursor(self) -> Self::Cursor { + []::new(self) + } + } + )* + } + }; +} + +for_all_primitives! { def_cursor } +for_all_primitives! { impl_key } +for_all_primitives! { impl_value } + +impl Key for Vec { + type Cursor = std::io::Cursor>; + + fn weight(&self) -> usize { + self.len() + } + + fn serialized_len(&self) -> usize { + self.len() + } + + fn read(buf: &[u8]) -> CodingResult { + Ok(buf.to_vec()) + } + + fn into_cursor(self) -> Self::Cursor { + std::io::Cursor::new(self) + } +} + +impl Value for Vec { + type Cursor = std::io::Cursor>; + + fn weight(&self) -> usize { + self.len() + } + + fn serialized_len(&self) -> usize { + self.len() + } + + fn read(buf: &[u8]) -> CodingResult { + Ok(buf.to_vec()) + } + + fn into_cursor(self) -> Self::Cursor { + std::io::Cursor::new(self) + } +} + +impl Cursor> for std::io::Cursor> { + fn into_inner(self) -> Vec { + self.into_inner() + } +} + +impl Key for Arc> { + type Cursor = ArcVecU8Cursor; + + fn weight(&self) -> usize { + self.len() + } + + fn serialized_len(&self) -> usize { + self.len() + } + + fn read(buf: &[u8]) -> CodingResult { + Ok(Arc::new(buf.to_vec())) + } + + fn into_cursor(self) -> Self::Cursor { + ArcVecU8Cursor::new(self) + } +} + +impl Value for Arc> { + type Cursor = ArcVecU8Cursor; + + fn weight(&self) -> usize { + self.len() + } + + fn serialized_len(&self) -> usize { + self.len() + } + + fn read(buf: &[u8]) -> CodingResult { + Ok(Arc::new(buf.to_vec())) + } + + fn into_cursor(self) -> Self::Cursor { + ArcVecU8Cursor::new(self) + } +} + +#[derive(Debug)] +pub struct ArcVecU8Cursor { + inner: Arc>, + pos: usize, +} + +impl ArcVecU8Cursor { + pub fn new(inner: Arc>) -> Self { + Self { inner, pos: 0 } + } +} + +impl std::io::Read for ArcVecU8Cursor { + fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { + let slice = self.inner.as_ref().as_slice(); + let len = std::cmp::min(slice.len() - self.pos, buf.len()); + buf.put_slice(&slice[self.pos..self.pos + len]); + self.pos += len; + Ok(len) + } +} + +impl Cursor>> for ArcVecU8Cursor { + fn into_inner(self) -> Arc> { + self.inner + } +} + +#[derive(Debug)] +pub struct PrimitiveCursorVoid; + +impl std::io::Read for PrimitiveCursorVoid { + fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { + Ok(0) + } +} + +impl Cursor<()> for PrimitiveCursorVoid { + fn into_inner(self) {} +} + +impl Key for () { + type Cursor = PrimitiveCursorVoid; + + fn weight(&self) -> usize { + 0 + } + + fn serialized_len(&self) -> usize { + 0 + } + + fn read(_buf: &[u8]) -> CodingResult { + Ok(()) + } + + fn into_cursor(self) -> Self::Cursor { + PrimitiveCursorVoid + } +} + +impl Value for () { + type Cursor = PrimitiveCursorVoid; + + fn weight(&self) -> usize { + 0 + } + + fn serialized_len(&self) -> usize { + 0 + } + + fn read(_buf: &[u8]) -> CodingResult { + Ok(()) + } + + fn into_cursor(self) -> Self::Cursor { + PrimitiveCursorVoid + } +} + +#[derive(Debug)] +pub struct UnimplementedCursor(PhantomData); + +impl std::io::Read for UnimplementedCursor { + fn read(&mut self, _: &mut [u8]) -> std::io::Result { + unimplemented!() + } +} + +impl Cursor for UnimplementedCursor { + fn into_inner(self) -> T { + unimplemented!() + } +} diff --git a/foyer-common/src/continuum.rs b/foyer-common/src/continuum.rs new file mode 100644 index 00000000..4efac1d4 --- /dev/null +++ b/foyer-common/src/continuum.rs @@ -0,0 +1,251 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + ops::Range, + sync::atomic::{AtomicU16, AtomicU32, AtomicU64, AtomicU8, AtomicUsize, Ordering}, +}; + +use itertools::Itertools; + +macro_rules! def_continuum { + ($( { $name:ident, $uint:ty, $atomic:ty }, )*) => { + $( + #[derive(Debug)] + pub struct $name { + slots: Vec<$atomic>, + capacity: $uint, + continuum: $atomic, + } + + impl $name { + pub fn new(capacity: $uint) -> Self { + let slots = (0..capacity).map(|_| <$atomic>::default()).collect_vec(); + let continuum = <$atomic>::default(); + Self { + slots, + capacity, + continuum, + } + } + + pub fn is_occupied(&self, start: $uint) -> bool { + !self.is_vacant(start) + } + + pub fn is_vacant(&self, start: $uint) -> bool { + let continuum = self.continuum.load(Ordering::Acquire); + if continuum + self.capacity > start { + return true; + } + + self.advance_until(|_, _| false, 0); + + let continuum = self.continuum.load(Ordering::Acquire); + continuum + self.capacity > start + } + + /// Submit a range. + pub fn submit(&self, range: Range<$uint>) { + debug_assert!(range.start < range.end); + + self.slots[self.slot(range.start)].store(range.end, Ordering::SeqCst); + } + + /// Submit a range, may advance continuum till the given range. + /// + /// Return `true` if advanced, else `false`. + pub fn submit_advance(&self, range: Range<$uint>) -> bool { + debug_assert!(range.start < range.end); + + let continuum = self.continuum.load(Ordering::Acquire); + + debug_assert!(continuum <= range.start, "assert continuum <= range.start failed: {} <= {}", continuum, range.start); + + if continuum == range.start { + // continuum can be advanced directly and exclusively + self.continuum.store(range.end, Ordering::Release); + true + } else { + let slot = &self.slots[self.slot(range.start)]; + slot.store(range.end, Ordering::Release); + let stop = move |current: $uint, _next: $uint| { + current > range.start + }; + self.advance_until(stop, 1) + } + } + + pub fn advance(&self) -> bool { + self.advance_until(|_, _| false, 0) + } + + pub fn continuum(&self) -> $uint { + self.continuum.load(Ordering::Acquire) + } + + fn slot(&self, position: $uint) -> usize { + (position % self.capacity) as usize + } + + /// `stop: Fn(continuum, next) -> bool`. + fn advance_until

(&self, stop: P, retry: usize) -> bool + where + P: Fn($uint, $uint) -> bool + Send + Sync + 'static, + { + let mut continuum = self.continuum.load(Ordering::Acquire); + let mut start = continuum; + + let mut times = 0; + loop { + let slot = &self.slots[self.slot(continuum)]; + + let next = slot.load(Ordering::Acquire); + + if next >= continuum + self.capacity { + // case 1: `range` >= `capacity` + // case 2: continuum has rotated before `next` is loaded + continuum = self.continuum.load(Ordering::Acquire); + if continuum != start { + start = continuum; + continue; + } + } + + if next <= continuum || stop(continuum, next) { + // nothing to advance + return false; + } + + // make sure `continuum` can be modified exclusively and lock + if let Ok(_) = slot.compare_exchange(next, continuum, Ordering::AcqRel, Ordering::Relaxed) { + // If this thread is scheduled for a long time after `continuum` is loaded, + // the `slot` may refer to a rotated slot with actual index `continuum + N * capacity`, + // and the loaded `continuum` lags. `continuum` needs to be checked if it is still behind + // the slot. + continuum = self.continuum.load(Ordering::Acquire); + if continuum == start { + // exclusive + continuum = next; + break; + } + } + + // prepare for the next retry + times += 1; + if times > retry { + return false; + } + + continuum = self.continuum.load(Ordering::Acquire); + if continuum == start { + return false; + } + start = continuum; + } + + loop { + let next = self.slots[self.slot(continuum)].load(Ordering::Relaxed); + + if next <= continuum || stop(continuum, next) { + break; + } + + continuum = next; + } + + debug_assert_eq!(start, self.continuum.load(Ordering::Acquire)); + + // modify continuum exclusively and unlock + self.continuum.store(continuum, Ordering::Release); + + if continuum == start { + return false; + } + return true; + } + } + )* + } +} + +macro_rules! for_all_primitives { + ($macro:ident) => { + $macro! { + { ContinuumU8, u8, AtomicU8 }, + { ContinuumU16, u16, AtomicU16 }, + { ContinuumU32, u32, AtomicU32 }, + { ContinuumU64, u64, AtomicU64 }, + { ContinuumUsize, usize, AtomicUsize }, + } + }; +} + +for_all_primitives! { def_continuum } + +#[cfg(test)] +mod tests { + use std::{sync::Arc, time::Duration}; + + use rand::{rngs::OsRng, Rng}; + use tokio::sync::Semaphore; + + use super::*; + + #[ignore] + #[tokio::test(flavor = "multi_thread")] + async fn test_continuum_fuzzy() { + const CAPACITY: u64 = 4096; + const CURRENCY: usize = 16; + const UNIT: u64 = 16; + const LOOP: usize = 1000; + + let s = Arc::new(Semaphore::new(CAPACITY as usize)); + let c = Arc::new(ContinuumU64::new(CAPACITY)); + let v = Arc::new(AtomicU64::new(0)); + + let tasks = (0..CURRENCY) + .map(|_| { + let s = s.clone(); + let c = c.clone(); + let v = v.clone(); + async move { + for _ in 0..LOOP { + let unit = OsRng.gen_range(1..UNIT); + let start = v.fetch_add(unit, Ordering::Relaxed); + let end = start + unit; + + let permit = s.acquire_many(unit as u32).await.unwrap(); + + let sleep = OsRng.gen_range(0..10); + tokio::time::sleep(Duration::from_millis(sleep)).await; + c.submit(start..end); + c.advance(); + + drop(permit); + } + } + }) + .collect_vec(); + + let handles = tasks.into_iter().map(tokio::spawn).collect_vec(); + for handle in handles { + handle.await.unwrap(); + } + + c.advance(); + + assert_eq!(v.load(Ordering::Relaxed), c.continuum()); + } +} diff --git a/foyer-common/src/erwlock.rs b/foyer-common/src/erwlock.rs new file mode 100644 index 00000000..9bd48f6f --- /dev/null +++ b/foyer-common/src/erwlock.rs @@ -0,0 +1,63 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use parking_lot::{lock_api::ArcRwLockWriteGuard, RawRwLock, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +pub trait ErwLockInner { + type R; + fn is_exclusive(&self, require: &Self::R) -> bool; +} + +#[derive(Debug)] +pub struct ErwLock { + inner: Arc>, +} + +impl Clone for ErwLock { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl ErwLock { + pub fn new(inner: T) -> Self { + Self { + inner: Arc::new(RwLock::new(inner)), + } + } + + pub fn read(&self) -> RwLockReadGuard<'_, T> { + self.inner.read() + } + + pub fn write(&self) -> RwLockWriteGuard<'_, T> { + self.inner.write() + } + + pub async fn exclusive(&self, require: &T::R) -> ArcRwLockWriteGuard { + loop { + { + let guard = self.inner.clone().write_arc(); + if guard.is_exclusive(require) { + return guard; + } + } + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + } + } +} diff --git a/foyer-common/src/lib.rs b/foyer-common/src/lib.rs new file mode 100644 index 00000000..adb30f00 --- /dev/null +++ b/foyer-common/src/lib.rs @@ -0,0 +1,26 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + +pub mod async_queue; +pub mod batch; +pub mod bits; +pub mod code; +pub mod continuum; +pub mod erwlock; +pub mod range; +pub mod rate; +pub mod rated_ticket; +pub mod runtime; diff --git a/foyer-common/src/range.rs b/foyer-common/src/range.rs new file mode 100644 index 00000000..0e887b3f --- /dev/null +++ b/foyer-common/src/range.rs @@ -0,0 +1,119 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ops::{Add, Bound, Range, RangeBounds, Sub}; + +mod private { + + pub trait ZeroOne { + fn zero() -> Self; + fn one() -> Self; + } + + macro_rules! impl_one { + ($($t:ty),*) => { + $( + impl ZeroOne for $t { + fn zero() -> Self { + 0 as $t + } + + fn one() -> Self { + 1 as $t + } + } + )* + }; + } + + macro_rules! for_all_num_type { + ($macro:ident) => { + $macro! { u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64 } + }; + } + + for_all_num_type! { impl_one } +} + +use private::ZeroOne; + +// TODO(MrCroxx): Use `trait_alias` after stable. +// pub trait Idx = +// PartialOrd + Add + Sub + Clone + Copy + Send + Sync + 'static + ZeroOne; + +pub trait RangeBoundsExt< + T: PartialOrd + Add + Sub + Clone + Copy + Send + Sync + 'static + ZeroOne, +>: RangeBounds +{ + fn start(&self) -> Option { + match self.start_bound() { + Bound::Included(v) => Some(*v), + Bound::Excluded(v) => Some(*v + ZeroOne::one()), + Bound::Unbounded => None, + } + } + + fn end(&self) -> Option { + match self.end_bound() { + Bound::Included(v) => Some(*v + ZeroOne::one()), + Bound::Excluded(v) => Some(*v), + Bound::Unbounded => None, + } + } + + fn start_with_bound(&self, bound: T) -> T { + self.start().unwrap_or(bound) + } + + fn end_with_bound(&self, bound: T) -> T { + self.end().unwrap_or(bound) + } + + fn bounds(&self, range: Range) -> Range { + let start = self.start_with_bound(range.start); + let end = self.end_with_bound(range.end); + start..end + } + + fn size(&self) -> Option { + let start = self.start()?; + let end = self.end()?; + Some(end - start) + } + + fn is_empty(&self) -> bool { + match self.size() { + Some(len) => len == ZeroOne::zero(), + None => false, + } + } + + fn is_full(&self) -> bool { + self.start_bound() == Bound::Unbounded && self.end_bound() == Bound::Unbounded + } + + fn map(&self, f: F) -> (Bound, Bound) + where + F: Fn(&T) -> R, + { + (self.start_bound().map(&f), self.end_bound().map(&f)) + } +} + +impl< + T: PartialOrd + Add + Sub + Clone + Copy + Send + Sync + 'static + ZeroOne, + RB: RangeBounds, + > RangeBoundsExt for RB +{ +} diff --git a/foyer-common/src/rate.rs b/foyer-common/src/rate.rs new file mode 100644 index 00000000..884fbc4a --- /dev/null +++ b/foyer-common/src/rate.rs @@ -0,0 +1,112 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::{Duration, Instant}; + +use parking_lot::Mutex; + +#[derive(Debug)] +pub struct RateLimiter { + inner: Mutex, + rate: f64, +} + +#[derive(Debug)] +struct Inner { + quota: f64, + + last: Instant, +} + +impl RateLimiter { + pub fn new(rate: f64) -> Self { + let inner = Inner { + quota: 0.0, + last: Instant::now(), + }; + Self { + rate, + inner: Mutex::new(inner), + } + } + + pub fn consume(&self, weight: f64) -> Option { + let mut inner = self.inner.lock(); + let now = Instant::now(); + let refill = now.duration_since(inner.last).as_secs_f64() * self.rate; + inner.last = now; + inner.quota = f64::min(inner.quota + refill, self.rate); + inner.quota -= weight; + if inner.quota >= 0.0 { + return None; + } + let wait = Duration::from_secs_f64((-inner.quota) / self.rate); + Some(wait) + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; + + use rand::{thread_rng, Rng}; + + use super::*; + + const ERATIO: f64 = 0.05; + const THREADS: usize = 8; + const RATE: usize = 1000; + const DURATION: Duration = Duration::from_secs(10); + + #[ignore] + #[test] + fn test_rate_limiter() { + let v = Arc::new(AtomicUsize::new(0)); + let limiter = Arc::new(RateLimiter::new(RATE as f64)); + let task = |rate: usize, v: Arc, limiter: Arc| { + let start = Instant::now(); + loop { + if start.elapsed() >= DURATION { + break; + } + if let Some(dur) = limiter.consume(rate as f64) { + std::thread::sleep(dur); + } + v.fetch_add(rate, Ordering::Relaxed); + } + }; + let mut handles = vec![]; + let mut rng = thread_rng(); + for _ in 0..THREADS { + let rate = rng.gen_range(10..20); + let handle = std::thread::spawn({ + let v = v.clone(); + let limiter = limiter.clone(); + move || task(rate, v, limiter) + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + let error = (v.load(Ordering::Relaxed) as isize - RATE as isize * DURATION.as_secs() as isize).unsigned_abs(); + let eratio = error as f64 / (RATE as f64 * DURATION.as_secs_f64()); + assert!(eratio < ERATIO, "eratio: {}, target: {}", eratio, ERATIO); + } +} diff --git a/foyer-common/src/rated_ticket.rs b/foyer-common/src/rated_ticket.rs new file mode 100644 index 00000000..95d2a235 --- /dev/null +++ b/foyer-common/src/rated_ticket.rs @@ -0,0 +1,178 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::Instant; + +use parking_lot::Mutex; + +#[derive(Debug)] +pub struct RatedTicket { + inner: Mutex, + rate: f64, +} + +#[derive(Debug)] +struct Inner { + quota: f64, + + last: Instant, +} + +impl RatedTicket { + pub fn new(rate: f64) -> Self { + let inner = Inner { + quota: 0.0, + last: Instant::now(), + }; + Self { + rate, + inner: Mutex::new(inner), + } + } + + pub fn probe(&self) -> bool { + let mut inner = self.inner.lock(); + + let now = Instant::now(); + let refill = now.duration_since(inner.last).as_secs_f64() * self.rate; + inner.last = now; + inner.quota = f64::min(inner.quota + refill, self.rate); + + inner.quota > 0.0 + } + + pub fn reduce(&self, weight: f64) { + self.inner.lock().quota -= weight; + } + + pub fn consume(&self, weight: f64) -> bool { + let mut inner = self.inner.lock(); + + let now = Instant::now(); + let refill = now.duration_since(inner.last).as_secs_f64() * self.rate; + inner.last = now; + inner.quota = f64::min(inner.quota + refill, self.rate); + + if inner.quota <= 0.0 { + return false; + } + + inner.quota -= weight; + + true + } +} + +#[cfg(test)] +mod tests { + use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, + }; + + use itertools::Itertools; + use rand::{thread_rng, Rng}; + + use super::*; + + #[ignore] + #[test] + fn test_rated_ticket_consume() { + test(consume) + } + + #[ignore] + #[test] + fn test_rated_ticket_probe_reduce() { + test(probe_reduce) + } + + fn test(f: F) + where + F: Fn(usize, &Arc, &Arc) + Send + Sync + Copy + 'static, + { + const CASES: usize = 10; + const ERATIO: f64 = 0.05; + + let handles = (0..CASES).map(|_| std::thread::spawn(move || case(f))).collect_vec(); + let mut eratios = vec![]; + for handle in handles { + let eratio = handle.join().unwrap(); + assert!(eratio < ERATIO, "eratio: {} < ERATIO: {}", eratio, ERATIO); + eratios.push(eratio); + } + println!("========== RatedTicket error ratio begin =========="); + for eratio in eratios { + println!("eratio: {eratio}"); + } + println!("=========== RatedTicket error ratio end ==========="); + } + + fn consume(weight: usize, v: &Arc, limiter: &Arc) { + if limiter.consume(weight as f64) { + v.fetch_add(weight, Ordering::Relaxed); + } + } + + fn probe_reduce(weight: usize, v: &Arc, limiter: &Arc) { + if limiter.probe() { + limiter.reduce(weight as f64); + v.fetch_add(weight, Ordering::Relaxed); + } + } + + fn case(f: F) -> f64 + where + F: Fn(usize, &Arc, &Arc) + Send + Sync + Copy + 'static, + { + const THREADS: usize = 8; + const RATE: usize = 1000; + const DURATION: Duration = Duration::from_secs(10); + + let v = Arc::new(AtomicUsize::new(0)); + let limiter = Arc::new(RatedTicket::new(RATE as f64)); + let task = |rate: usize, v: Arc, limiter: Arc, f: F| { + let start = Instant::now(); + let mut rng = thread_rng(); + loop { + if start.elapsed() >= DURATION { + break; + } + std::thread::sleep(Duration::from_millis(rng.gen_range(1..100))); + f(rate, &v, &limiter) + } + }; + let mut handles = vec![]; + let mut rng = thread_rng(); + for _ in 0..THREADS { + let rate = rng.gen_range(10..20); + let handle = std::thread::spawn({ + let v = v.clone(); + let limiter = limiter.clone(); + move || task(rate, v, limiter, f) + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } + + let error = (v.load(Ordering::Relaxed) as isize - RATE as isize * DURATION.as_secs() as isize).unsigned_abs(); + error as f64 / (RATE as f64 * DURATION.as_secs_f64()) + } +} diff --git a/foyer-common/src/runtime.rs b/foyer-common/src/runtime.rs new file mode 100644 index 00000000..703171b2 --- /dev/null +++ b/foyer-common/src/runtime.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fmt::Debug, + mem::ManuallyDrop, + ops::{Deref, DerefMut}, +}; + +use tokio::runtime::Runtime; + +/// A wrapper around [`Runtime`] that shuts down the runtime in the background when dropped. +/// +/// This is necessary because directly dropping a nested runtime is not allowed in a parent runtime. +pub struct BackgroundShutdownRuntime(ManuallyDrop); + +impl Debug for BackgroundShutdownRuntime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("BackgroundShutdownRuntime").finish() + } +} + +impl Drop for BackgroundShutdownRuntime { + fn drop(&mut self) { + // Safety: The runtime is only dropped once here. + let runtime = unsafe { ManuallyDrop::take(&mut self.0) }; + + #[cfg(madsim)] + drop(runtime); + #[cfg(not(madsim))] + runtime.shutdown_background(); + } +} + +impl Deref for BackgroundShutdownRuntime { + type Target = Runtime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BackgroundShutdownRuntime { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for BackgroundShutdownRuntime { + fn from(runtime: Runtime) -> Self { + Self(ManuallyDrop::new(runtime)) + } +} diff --git a/foyer-experimental-bench/Cargo.toml b/foyer-experimental-bench/Cargo.toml new file mode 100644 index 00000000..b21e44e2 --- /dev/null +++ b/foyer-experimental-bench/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "foyer-experimental-bench" +version = "0.0.0" +edition = "2021" +authors = ["MrCroxx "] +description = "storage engine bench tool for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +publish = false +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +autobenches = false + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +anyhow = "1" +bytesize = "1" +clap = { version = "4", features = ["derive"] } +console-subscriber = { version = "0.2", optional = true } +foyer-common = { version = "0.5", path = "../foyer-common" } +foyer-experimental = { version = "*", path = "../foyer-experimental" } +foyer-intrusive = { version = "0.4", path = "../foyer-intrusive" } +foyer-storage = { version = "0.6", path = "../foyer-storage" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +futures = "0.3" +hdrhistogram = "7" +http-body-util = "0.1" +hyper = { version = "1.0", features = ["server", "http1", "http2"] } +hyper-util = { version = "0.1", features = [ + "server", + "server-auto", + "http1", + "http2", + "tokio", +] } +itertools = "0.12" +libc = "0.2" +nix = { version = "0.28", features = ["fs", "mman"] } +opentelemetry = { version = "0.22", optional = true } +opentelemetry-otlp = { version = "0.15.0", optional = true } +opentelemetry-semantic-conventions = { version = "0.14", optional = true } +opentelemetry_sdk = { version = "0.22", features = [ + "rt-tokio", + "trace", +], optional = true } +parking_lot = "0.12" +prometheus = "0.13" +rand = "0.8.5" +tokio = { workspace = true } +tracing = "0.1" +tracing-opentelemetry = { version = "0.23", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[features] +deadlock = ["parking_lot/deadlock_detection", "foyer-storage/deadlock"] +tokio-console = ["console-subscriber"] +trace = [ + "opentelemetry", + "opentelemetry_sdk", + "opentelemetry-otlp", + "tracing-opentelemetry", + "opentelemetry-semantic-conventions", +] + +[[bin]] +name = "wal-bench" +path = "benches/wal-bench.rs" +bench = false diff --git a/foyer-experimental-bench/benches/wal-bench.rs b/foyer-experimental-bench/benches/wal-bench.rs new file mode 100644 index 00000000..4af9b033 --- /dev/null +++ b/foyer-experimental-bench/benches/wal-bench.rs @@ -0,0 +1,125 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + path::PathBuf, + sync::Arc, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +use clap::Parser; +use foyer_common::runtime::BackgroundShutdownRuntime; +use foyer_experimental::{ + metrics::METRICS, + wal::{Tombstone, TombstoneLog, TombstoneLogConfig}, +}; +use foyer_experimental_bench::{export::MetricsExporter, init_logger, io_monitor, IoMonitorConfig}; +use itertools::Itertools; +use rand::{rngs::StdRng, Rng, SeedableRng}; + +#[derive(Parser, Debug, Clone)] +#[command(author, version, about)] +pub struct Args { + /// dir for cache data + #[arg(short, long)] + dir: String, + + /// writer concurrency + #[arg(short, long, default_value_t = 1024)] + concurrency: usize, + + /// time (s) + #[arg(short, long, default_value_t = 60)] + time: usize, + + /// (s) + #[arg(long, default_value_t = 2)] + report_interval: u64, + + #[arg(long, default_value_t = false)] + metrics: bool, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + println!("{:#?}", args); + + init_logger(); + + if args.metrics { + MetricsExporter::init("0.0.0.0:19970".parse().unwrap()); + } + + let guard = io_monitor(IoMonitorConfig { + dir: PathBuf::from(&args.dir), + interval: Duration::from_secs(args.report_interval), + total_secs: args.time, + }); + + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + let rt = BackgroundShutdownRuntime::from(rt); + let rt = Arc::new(rt); + + let config = TombstoneLogConfig { + id: 0, + dir: args.dir.clone().into(), + metrics: Arc::new(METRICS.metrics("wal-bench")), + }; + let log = TombstoneLog::open(config).await.unwrap(); + + let handles = (0..args.concurrency) + .map(|_| { + let log = log.clone(); + let args = args.clone(); + let rt = rt.clone(); + tokio::spawn(async move { + write(log.clone(), args, rt).await; + }) + }) + .collect_vec(); + + for handle in handles { + handle.await.unwrap(); + } + + drop(guard); + + log.close().await.unwrap(); +} + +async fn write(log: TombstoneLog, args: Args, rt: Arc) { + let start = Instant::now(); + let mut log = log; + + let mut rng = StdRng::seed_from_u64(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() as _); + loop { + if start.elapsed() >= Duration::from_secs(args.time as _) { + return; + } + + let tombstone = Tombstone::new(rng.gen(), rng.gen()); + log = rt + .spawn(async move { + log.append(tombstone).await.unwrap(); + log + }) + .await + .unwrap(); + } +} diff --git a/foyer-experimental-bench/etc/sample.txt b/foyer-experimental-bench/etc/sample.txt new file mode 100644 index 00000000..ab729b88 --- /dev/null +++ b/foyer-experimental-bench/etc/sample.txt @@ -0,0 +1,676 @@ +!!! This is just a sample text, not a real license. !!! + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/foyer-experimental-bench/src/analyze.rs b/foyer-experimental-bench/src/analyze.rs new file mode 100644 index 00000000..4c50cfa5 --- /dev/null +++ b/foyer-experimental-bench/src/analyze.rs @@ -0,0 +1,333 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + path::Path, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use bytesize::ByteSize; +use hdrhistogram::Histogram; +use parking_lot::RwLock; +use tokio::sync::broadcast; + +use crate::utils::{iostat, IoStat}; + +const SECTOR_SIZE: usize = 512; + +// latencies are measured by 'us' +#[derive(Clone, Copy, Debug)] +pub struct Analysis { + disk_read_iops: f64, + disk_read_throughput: f64, + disk_write_iops: f64, + disk_write_throughput: f64, + + insert_iops: f64, + insert_throughput: f64, + insert_lat_p50: u64, + insert_lat_p90: u64, + insert_lat_p99: u64, + insert_lat_p999: u64, + insert_lat_p9999: u64, + insert_lat_p99999: u64, + insert_lat_pmax: u64, + + get_iops: f64, + get_miss: f64, + get_throughput: f64, + get_miss_lat_p50: u64, + get_miss_lat_p90: u64, + get_miss_lat_p99: u64, + get_miss_lat_p999: u64, + get_miss_lat_p9999: u64, + get_miss_lat_p99999: u64, + get_miss_lat_pmax: u64, + get_hit_lat_p50: u64, + get_hit_lat_p90: u64, + get_hit_lat_p99: u64, + get_hit_lat_p999: u64, + get_hit_lat_p9999: u64, + get_hit_lat_p99999: u64, + get_hit_lat_pmax: u64, +} + +#[derive(Default, Clone, Copy, Debug)] +pub struct MetricsDump { + pub insert_ios: usize, + pub insert_bytes: usize, + pub insert_lat_p50: u64, + pub insert_lat_p90: u64, + pub insert_lat_p99: u64, + pub insert_lat_p999: u64, + pub insert_lat_p9999: u64, + pub insert_lat_p99999: u64, + pub insert_lat_pmax: u64, + + pub get_ios: usize, + pub get_miss_ios: usize, + pub get_bytes: usize, + pub get_hit_lat_p50: u64, + pub get_hit_lat_p90: u64, + pub get_hit_lat_p99: u64, + pub get_hit_lat_p999: u64, + pub get_hit_lat_p9999: u64, + pub get_hit_lat_p99999: u64, + pub get_hit_lat_pmax: u64, + pub get_miss_lat_p50: u64, + pub get_miss_lat_p90: u64, + pub get_miss_lat_p99: u64, + pub get_miss_lat_p999: u64, + pub get_miss_lat_p9999: u64, + pub get_miss_lat_p99999: u64, + pub get_miss_lat_pmax: u64, +} + +#[derive(Clone, Debug)] +pub struct Metrics { + pub insert_ios: Arc, + pub insert_bytes: Arc, + pub insert_lats: Arc>>, + + pub get_ios: Arc, + pub get_bytes: Arc, + pub get_miss_ios: Arc, + pub get_hit_lats: Arc>>, + pub get_miss_lats: Arc>>, +} + +impl Default for Metrics { + fn default() -> Self { + Self { + insert_ios: Arc::new(AtomicUsize::new(0)), + insert_bytes: Arc::new(AtomicUsize::new(0)), + insert_lats: Arc::new(RwLock::new(Histogram::new_with_bounds(1, 10_000_000, 2).unwrap())), + + get_ios: Arc::new(AtomicUsize::new(0)), + get_bytes: Arc::new(AtomicUsize::new(0)), + get_miss_ios: Arc::new(AtomicUsize::new(0)), + get_hit_lats: Arc::new(RwLock::new(Histogram::new_with_bounds(1, 10_000_000, 2).unwrap())), + get_miss_lats: Arc::new(RwLock::new(Histogram::new_with_bounds(1, 10_000_000, 2).unwrap())), + } + } +} + +impl Metrics { + pub fn dump(&self) -> MetricsDump { + let insert_lats = self.insert_lats.read(); + let get_hit_lats = self.get_hit_lats.read(); + let get_miss_lats = self.get_miss_lats.read(); + + MetricsDump { + insert_ios: self.insert_ios.load(Ordering::Relaxed), + insert_bytes: self.insert_bytes.load(Ordering::Relaxed), + insert_lat_p50: insert_lats.value_at_quantile(0.5), + insert_lat_p90: insert_lats.value_at_quantile(0.9), + insert_lat_p99: insert_lats.value_at_quantile(0.99), + insert_lat_p999: insert_lats.value_at_quantile(0.999), + insert_lat_p9999: insert_lats.value_at_quantile(0.9999), + insert_lat_p99999: insert_lats.value_at_quantile(0.99999), + insert_lat_pmax: insert_lats.max(), + + get_ios: self.get_ios.load(Ordering::Relaxed), + get_miss_ios: self.get_miss_ios.load(Ordering::Relaxed), + get_bytes: self.get_bytes.load(Ordering::Relaxed), + get_hit_lat_p50: get_hit_lats.value_at_quantile(0.5), + get_hit_lat_p90: get_hit_lats.value_at_quantile(0.9), + get_hit_lat_p99: get_hit_lats.value_at_quantile(0.99), + get_hit_lat_p999: get_hit_lats.value_at_quantile(0.999), + get_hit_lat_p9999: get_hit_lats.value_at_quantile(0.9999), + get_hit_lat_p99999: get_hit_lats.value_at_quantile(0.99999), + get_hit_lat_pmax: get_hit_lats.max(), + get_miss_lat_p50: get_miss_lats.value_at_quantile(0.5), + get_miss_lat_p90: get_miss_lats.value_at_quantile(0.9), + get_miss_lat_p99: get_miss_lats.value_at_quantile(0.99), + get_miss_lat_p999: get_miss_lats.value_at_quantile(0.999), + get_miss_lat_p9999: get_miss_lats.value_at_quantile(0.9999), + get_miss_lat_p99999: get_miss_lats.value_at_quantile(0.99999), + get_miss_lat_pmax: get_miss_lats.max(), + } + } +} + +impl std::fmt::Display for Analysis { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let disk_read_throughput = ByteSize::b(self.disk_read_throughput as u64); + let disk_write_throughput = ByteSize::b(self.disk_write_throughput as u64); + let disk_total_throughput = disk_read_throughput + disk_write_throughput; + + // disk statics + writeln!(f, "disk total iops: {:.1}", self.disk_write_iops + self.disk_read_iops)?; + writeln!( + f, + "disk total throughput: {}/s", + disk_total_throughput.to_string_as(true) + )?; + writeln!(f, "disk read iops: {:.1}", self.disk_read_iops)?; + writeln!(f, "disk read throughput: {}/s", disk_read_throughput.to_string_as(true))?; + writeln!(f, "disk write iops: {:.1}", self.disk_write_iops)?; + writeln!( + f, + "disk write throughput: {}/s", + disk_write_throughput.to_string_as(true) + )?; + + // insert statics + let insert_throughput = ByteSize::b(self.insert_throughput as u64); + writeln!(f, "insert iops: {:.1}/s", self.insert_iops)?; + writeln!(f, "insert throughput: {}/s", insert_throughput.to_string_as(true))?; + writeln!(f, "insert lat p50: {}us", self.insert_lat_p50)?; + writeln!(f, "insert lat p90: {}us", self.insert_lat_p90)?; + writeln!(f, "insert lat p99: {}us", self.insert_lat_p99)?; + writeln!(f, "insert lat p999: {}us", self.insert_lat_p999)?; + writeln!(f, "insert lat p9999: {}us", self.insert_lat_p9999)?; + writeln!(f, "insert lat p99999: {}us", self.insert_lat_p99999)?; + writeln!(f, "insert lat pmax: {}us", self.insert_lat_pmax)?; + + // get statics + let get_throughput = ByteSize::b(self.get_throughput as u64); + writeln!(f, "get iops: {:.1}/s", self.get_iops)?; + writeln!(f, "get miss: {:.2}% ", self.get_miss * 100f64)?; + writeln!(f, "get throughput: {}/s", get_throughput.to_string_as(true))?; + writeln!(f, "get hit lat p50: {}us", self.get_hit_lat_p50)?; + writeln!(f, "get hit lat p90: {}us", self.get_hit_lat_p90)?; + writeln!(f, "get hit lat p99: {}us", self.get_hit_lat_p99)?; + writeln!(f, "get hit lat p999: {}us", self.get_hit_lat_p999)?; + writeln!(f, "get hit lat p9999: {}us", self.get_hit_lat_p9999)?; + writeln!(f, "get hit lat p99999: {}us", self.get_hit_lat_p99999)?; + writeln!(f, "get hit lat pmax: {}us", self.get_hit_lat_pmax)?; + writeln!(f, "get miss lat p50: {}us", self.get_miss_lat_p50)?; + writeln!(f, "get miss lat p90: {}us", self.get_miss_lat_p90)?; + writeln!(f, "get miss lat p99: {}us", self.get_miss_lat_p99)?; + writeln!(f, "get miss lat p999: {}us", self.get_miss_lat_p999)?; + writeln!(f, "get miss lat p9999: {}us", self.get_miss_lat_p9999)?; + writeln!(f, "get miss lat p99999: {}us", self.get_miss_lat_p99999)?; + writeln!(f, "get miss lat pmax: {}us", self.get_miss_lat_pmax)?; + + Ok(()) + } +} + +pub fn analyze( + duration: Duration, + iostat_start: &IoStat, + iostat_end: &IoStat, + metrics_dump_start: &MetricsDump, + metrics_dump_end: &MetricsDump, +) -> Analysis { + let secs = duration.as_secs_f64(); + let disk_read_iops = (iostat_end.read_ios - iostat_start.read_ios) as f64 / secs; + let disk_read_throughput = (iostat_end.read_sectors - iostat_start.read_sectors) as f64 * SECTOR_SIZE as f64 / secs; + let disk_write_iops = (iostat_end.write_ios - iostat_start.write_ios) as f64 / secs; + let disk_write_throughput = + (iostat_end.write_sectors - iostat_start.write_sectors) as f64 * SECTOR_SIZE as f64 / secs; + + let insert_iops = (metrics_dump_end.insert_ios - metrics_dump_start.insert_ios) as f64 / secs; + let insert_throughput = (metrics_dump_end.insert_bytes - metrics_dump_start.insert_bytes) as f64 / secs; + + let get_iops = (metrics_dump_end.get_ios - metrics_dump_start.get_ios) as f64 / secs; + let get_miss = (metrics_dump_end.get_miss_ios - metrics_dump_start.get_miss_ios) as f64 + / (metrics_dump_end.get_ios - metrics_dump_start.get_ios) as f64; + let get_throughput = (metrics_dump_end.get_bytes - metrics_dump_start.get_bytes) as f64 / secs; + + Analysis { + disk_read_iops, + disk_read_throughput, + disk_write_iops, + disk_write_throughput, + + insert_iops, + insert_throughput, + insert_lat_p50: metrics_dump_end.insert_lat_p50, + insert_lat_p90: metrics_dump_end.insert_lat_p90, + insert_lat_p99: metrics_dump_end.insert_lat_p99, + insert_lat_p999: metrics_dump_end.insert_lat_p999, + insert_lat_p9999: metrics_dump_end.insert_lat_p9999, + insert_lat_p99999: metrics_dump_end.insert_lat_p99999, + insert_lat_pmax: metrics_dump_end.insert_lat_pmax, + + get_iops, + get_miss, + get_throughput, + get_hit_lat_p50: metrics_dump_end.get_hit_lat_p50, + get_hit_lat_p90: metrics_dump_end.get_hit_lat_p90, + get_hit_lat_p99: metrics_dump_end.get_hit_lat_p99, + get_hit_lat_p999: metrics_dump_end.get_hit_lat_p999, + get_hit_lat_p9999: metrics_dump_end.get_hit_lat_p9999, + get_hit_lat_p99999: metrics_dump_end.get_hit_lat_p99999, + get_hit_lat_pmax: metrics_dump_end.get_hit_lat_pmax, + get_miss_lat_p50: metrics_dump_end.get_miss_lat_p50, + get_miss_lat_p90: metrics_dump_end.get_miss_lat_p90, + get_miss_lat_p99: metrics_dump_end.get_miss_lat_p99, + get_miss_lat_p999: metrics_dump_end.get_miss_lat_p999, + get_miss_lat_p9999: metrics_dump_end.get_miss_lat_p9999, + get_miss_lat_p99999: metrics_dump_end.get_miss_lat_p99999, + get_miss_lat_pmax: metrics_dump_end.get_miss_lat_pmax, + } +} + +pub async fn monitor( + iostat_path: impl AsRef, + interval: Duration, + total_secs: u64, + metrics: Metrics, + mut stop: broadcast::Receiver<()>, +) { + let mut stat = iostat(&iostat_path); + let mut metrics_dump = metrics.dump(); + + let start = Instant::now(); + + loop { + let now = Instant::now(); + match stop.try_recv() { + Err(broadcast::error::TryRecvError::Empty) => {} + _ => return, + } + + tokio::time::sleep(interval).await; + let new_stat = iostat(&iostat_path); + let new_metrics_dump = metrics.dump(); + let analysis = analyze( + // interval may have ~ +7% error + now.elapsed(), + &stat, + &new_stat, + &metrics_dump, + &new_metrics_dump, + ); + println!("[{}s/{}s]", start.elapsed().as_secs(), total_secs); + println!("{}", analysis); + stat = new_stat; + metrics_dump = new_metrics_dump; + } +} diff --git a/foyer-experimental-bench/src/export.rs b/foyer-experimental-bench/src/export.rs new file mode 100644 index 00000000..884aeb46 --- /dev/null +++ b/foyer-experimental-bench/src/export.rs @@ -0,0 +1,68 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::net::SocketAddr; + +use foyer_storage::metrics::get_metrics_registry; +use http_body_util::Full; +use hyper::{ + body::{Body, Bytes}, + header::CONTENT_TYPE, + service::service_fn, + Request, Response, +}; +use prometheus::{Encoder, TextEncoder}; +use tokio::net::TcpListener; + +pub struct MetricsExporter; + +impl MetricsExporter { + pub fn init(addr: SocketAddr) { + tokio::spawn(async move { + tracing::info!("Prometheus service is set up on http://{}", addr); + + let listener = TcpListener::bind(addr).await.unwrap(); + loop { + let stream = match listener.accept().await { + Ok((stream, _addr)) => stream, + Err(e) => { + tracing::error!("accept conn error: {}", e); + continue; + } + }; + let io = hyper_util::rt::TokioIo::new(stream); + tokio::spawn(async move { + if let Err(e) = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()) + .serve_connection(io, service_fn(Self::serve)) + .await + { + tracing::error!("Prometheus service error: {}", e); + } + }); + } + }); + } + + async fn serve(_request: Request) -> anyhow::Result>> { + let encoder = TextEncoder::new(); + let mut buffer = Vec::with_capacity(4096); + let metrics = get_metrics_registry().gather(); + encoder.encode(&metrics, &mut buffer)?; + let response = Response::builder() + .status(200) + .header(CONTENT_TYPE, encoder.format_type()) + .body(Full::new(Bytes::from(buffer)))?; + Ok(response) + } +} diff --git a/foyer-experimental-bench/src/lib.rs b/foyer-experimental-bench/src/lib.rs new file mode 100644 index 00000000..1509de6c --- /dev/null +++ b/foyer-experimental-bench/src/lib.rs @@ -0,0 +1,189 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod analyze; +pub mod export; +pub mod rate; +pub mod text; +pub mod utils; + +use std::{ + path::PathBuf, + time::{Duration, Instant}, +}; + +use analyze::{analyze, Metrics, MetricsDump}; +use tokio::task::JoinHandle; +use utils::{iostat, IoStat}; + +#[cfg(feature = "tokio-console")] +pub fn init_logger() { + console_subscriber::init(); +} + +#[cfg(feature = "trace")] +pub fn init_logger() { + use opentelemetry_sdk::{ + trace::{BatchConfigBuilder, Config}, + Resource, + }; + use opentelemetry_semantic_conventions::resource::SERVICE_NAME; + use tracing::Level; + use tracing_subscriber::{filter::Targets, prelude::*}; + + let trace_config = Config::default().with_resource(Resource::new(vec![opentelemetry::KeyValue::new( + SERVICE_NAME, + "foyer-storage-bench", + )])); + let batch_config = BatchConfigBuilder::default() + .with_max_queue_size(1048576) + .with_max_export_batch_size(4096) + .with_max_concurrent_exports(4) + .build(); + + let tracer = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter(opentelemetry_otlp::new_exporter().tonic()) + .with_trace_config(trace_config) + .with_batch_config(batch_config) + .install_batch(opentelemetry_sdk::runtime::Tokio) + .unwrap(); + let opentelemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with( + Targets::new() + .with_target("foyer_storage", Level::DEBUG) + .with_target("foyer_common", Level::DEBUG) + .with_target("foyer_intrusive", Level::DEBUG) + .with_target("foyer_storage_bench", Level::DEBUG), + ) + .with(opentelemetry_layer) + .init(); +} + +#[cfg(not(any(feature = "tokio-console", feature = "trace")))] +pub fn init_logger() { + use tracing_subscriber::{prelude::*, EnvFilter}; + + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + // .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .with_line_number(true), + ) + .with(EnvFilter::from_default_env()) + .init(); +} + +#[derive(Debug, Clone)] +pub struct IoMonitorConfig { + pub dir: PathBuf, + pub interval: Duration, + pub total_secs: usize, +} + +#[cfg(not(target_os = "linux"))] +pub fn io_monitor(_: IoMonitorConfig) -> IoMonitorGuard { + panic!("bench tool io monitor only supports linux") +} + +#[cfg(target_os = "linux")] +pub fn io_monitor(config: IoMonitorConfig) -> IoMonitorGuard { + use utils::{detect_fs_type, io_stat_path, FsType}; + + let fs_type = detect_fs_type(&config.dir); + let io_stat_path = match fs_type { + FsType::Xfs | FsType::Ext4 => io_stat_path(&config.dir), + _ => panic!("bench tool io monitor only supports ext4 and xfs"), + }; + + let metrics = Metrics::default(); + + let start_io_stat = iostat(&io_stat_path); + let start_metrics_dump = metrics.dump(); + let start = Instant::now(); + + let handle = { + let metrics = metrics.clone(); + let config = config.clone(); + let io_stat_path = io_stat_path.clone(); + tokio::spawn(async move { + let mut last_io_stat = start_io_stat; + let mut last_metrics_dump = start_metrics_dump; + loop { + let now = Instant::now(); + tokio::time::sleep(config.interval).await; + let new_io_stat = iostat(&io_stat_path); + let new_metrics_dump = metrics.dump(); + let analysis = analyze( + now.elapsed(), + &last_io_stat, + &new_io_stat, + &last_metrics_dump, + &new_metrics_dump, + ); + println!("Report [{}s/{}s]", start.elapsed().as_secs(), config.total_secs); + println!("{}", analysis); + last_io_stat = new_io_stat; + last_metrics_dump = new_metrics_dump; + } + }) + }; + + IoMonitorGuard { + config, + io_stat_path, + + metrics, + + start, + start_io_stat, + start_metrics_dump, + + handle, + } +} + +#[allow(dead_code)] +#[must_use] +pub struct IoMonitorGuard { + config: IoMonitorConfig, + io_stat_path: PathBuf, + + metrics: Metrics, + + start: Instant, + start_io_stat: IoStat, + start_metrics_dump: MetricsDump, + + handle: JoinHandle<()>, +} + +impl Drop for IoMonitorGuard { + fn drop(&mut self) { + self.handle.abort(); + let end_io_stat = iostat(&self.io_stat_path); + let end_metrics_dump = self.metrics.dump(); + let analysis = analyze( + self.start.elapsed(), + &self.start_io_stat, + &end_io_stat, + &self.start_metrics_dump, + &end_metrics_dump, + ); + let elapsed = self.start.elapsed(); + println!("Summary [{}s]", elapsed.as_secs()); + println!("{}", analysis); + } +} diff --git a/foyer-experimental-bench/src/rate.rs b/foyer-experimental-bench/src/rate.rs new file mode 100644 index 00000000..e7d86a34 --- /dev/null +++ b/foyer-experimental-bench/src/rate.rs @@ -0,0 +1,59 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::{Duration, Instant}; + +pub struct RateLimiter { + capacity: f64, + quota: f64, + + last: Instant, +} + +impl RateLimiter { + pub fn new(capacity: f64) -> Self { + Self { + capacity, + quota: 0.0, + last: Instant::now(), + } + } + + pub fn consume(&mut self, weight: f64) -> Option { + let now = Instant::now(); + let refill = now.duration_since(self.last).as_secs_f64() * self.capacity; + self.last = now; + self.quota = f64::min(self.quota + refill, self.capacity); + self.quota -= weight; + if self.quota >= 0.0 { + return None; + } + let wait = Duration::from_secs_f64((-self.quota) / self.capacity); + Some(wait) + } +} diff --git a/foyer-experimental-bench/src/text.rs b/foyer-experimental-bench/src/text.rs new file mode 100644 index 00000000..2ec8f8f2 --- /dev/null +++ b/foyer-experimental-bench/src/text.rs @@ -0,0 +1,28 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const TEXT: &[u8] = include_bytes!("../etc/sample.txt"); + +pub fn text(offset: usize, len: usize) -> Vec { + let mut res = Vec::with_capacity(len); + let mut cursor = offset % TEXT.len(); + let mut remain = len; + while remain > 0 { + let bytes = std::cmp::min(remain, TEXT.len() - cursor); + res.extend(&TEXT[cursor..cursor + bytes]); + cursor = (cursor + bytes) % TEXT.len(); + remain -= bytes; + } + res +} diff --git a/foyer-experimental-bench/src/utils.rs b/foyer-experimental-bench/src/utils.rs new file mode 100644 index 00000000..17a5f7e2 --- /dev/null +++ b/foyer-experimental-bench/src/utils.rs @@ -0,0 +1,170 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::{Path, PathBuf}; + +use itertools::Itertools; +use nix::{fcntl::readlink, sys::stat::stat}; + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum FsType { + Xfs, + Ext4, + Btrfs, + Tmpfs, + Others, +} + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] +pub fn detect_fs_type(path: impl AsRef) -> FsType { + #[cfg(target_os = "linux")] + { + use nix::sys::statfs::{statfs, BTRFS_SUPER_MAGIC, EXT4_SUPER_MAGIC, TMPFS_MAGIC, XFS_SUPER_MAGIC}; + let fs_stat = statfs(path.as_ref()).unwrap(); + match fs_stat.filesystem_type() { + XFS_SUPER_MAGIC => FsType::Xfs, + EXT4_SUPER_MAGIC => FsType::Ext4, + BTRFS_SUPER_MAGIC => FsType::Btrfs, + TMPFS_MAGIC => FsType::Tmpfs, + _ => FsType::Others, + } + } + + #[cfg(not(target_os = "linux"))] + FsType::Others +} + +/// Given a normal file path, returns the containing block device static file path (of the +/// partition). +pub fn io_stat_path(path: impl AsRef) -> PathBuf { + let st_dev = stat(path.as_ref()).unwrap().st_dev; + + let major = unsafe { libc::major(st_dev) }; + let minor = unsafe { libc::minor(st_dev) }; + + let dev = PathBuf::from("/dev/block").join(format!("{}:{}", major, minor)); + + let linkname = readlink(&dev).unwrap(); + let devname = Path::new(linkname.as_os_str()).file_name().unwrap(); + dev_stat_path(devname.to_str().unwrap()) +} + +pub fn dev_stat_path(devname: &str) -> PathBuf { + let classpath = Path::new("/sys/class/block").join(devname); + let devclass = readlink(&classpath).unwrap(); + + let devpath = Path::new(&devclass); + Path::new("/sys") + .join(devpath.strip_prefix("../..").unwrap()) + .join("stat") +} + +#[derive(Debug, Clone, Copy)] +pub struct IoStat { + /// read I/Os requests number of read I/Os processed + pub read_ios: usize, + /// read merges requests number of read I/Os merged with in-queue I/O + pub read_merges: usize, + /// read sectors sectors number of sectors read + pub read_sectors: usize, + /// read ticks milliseconds total wait time for read requests + pub read_ticks: usize, + /// write I/Os requests number of write I/Os processed + pub write_ios: usize, + /// write merges requests number of write I/Os merged with in-queue I/O + pub write_merges: usize, + /// write sectors sectors number of sectors written + pub write_sectors: usize, + /// write ticks milliseconds total wait time for write requests + pub write_ticks: usize, + /// in_flight requests number of I/Os currently in flight + pub in_flight: usize, + /// io_ticks milliseconds total time this block device has been active + pub io_ticks: usize, + /// time_in_queue milliseconds total wait time for all requests + pub time_in_queue: usize, + /// discard I/Os requests number of discard I/Os processed + pub discard_ios: usize, + /// discard merges requests number of discard I/Os merged with in-queue I/O + pub discard_merges: usize, + /// discard sectors sectors number of sectors discarded + pub discard_sectors: usize, + /// discard ticks milliseconds total wait time for discard requests + pub discard_ticks: usize, + /// flush I/Os requests number of flush I/Os processed + pub flush_ios: usize, + /// flush ticks milliseconds total wait time for flush requests + pub flush_ticks: usize, +} + +/// Given the device static file path and get the iostat. +pub fn iostat(path: impl AsRef) -> IoStat { + let content = std::fs::read_to_string(path.as_ref()).unwrap(); + let nums = content.split_ascii_whitespace().collect_vec(); + + let read_ios = nums[0].parse().unwrap(); + let read_merges = nums[1].parse().unwrap(); + let read_sectors = nums[2].parse().unwrap(); + let read_ticks = nums[3].parse().unwrap(); + let write_ios = nums[4].parse().unwrap(); + let write_merges = nums[5].parse().unwrap(); + let write_sectors = nums[6].parse().unwrap(); + let write_ticks = nums[7].parse().unwrap(); + let in_flight = nums[8].parse().unwrap(); + let io_ticks = nums[9].parse().unwrap(); + let time_in_queue = nums[10].parse().unwrap(); + let discard_ios = nums[11].parse().unwrap(); + let discard_merges = nums[12].parse().unwrap(); + let discard_sectors = nums[13].parse().unwrap(); + let discard_ticks = nums[14].parse().unwrap(); + let flush_ios = nums[15].parse().unwrap(); + let flush_ticks = nums[16].parse().unwrap(); + + IoStat { + read_ios, + read_merges, + read_sectors, + read_ticks, + write_ios, + write_merges, + write_sectors, + write_ticks, + in_flight, + io_ticks, + time_in_queue, + discard_ios, + discard_merges, + discard_sectors, + discard_ticks, + flush_ios, + flush_ticks, + } +} diff --git a/foyer-experimental/Cargo.toml b/foyer-experimental/Cargo.toml new file mode 100644 index 00000000..f60cac62 --- /dev/null +++ b/foyer-experimental/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "foyer-experimental" +version = "0.0.0" +edition = "2021" +authors = ["MrCroxx "] +description = "experimental components for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +publish = false +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +anyhow = "1.0" +bytes = "1" +crossbeam = { version = "0.8", features = ["std", "crossbeam-channel"] } +foyer-common = { version = "0.5", path = "../foyer-common" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +lazy_static = "1" +parking_lot = { version = "0.12", features = ["arc_lock"] } +paste = "1.0" +prometheus = "0.13" +thiserror = "1" +tokio = { workspace = true } +tracing = "0.1" + +[dev-dependencies] +bytesize = "1" +clap = { version = "4", features = ["derive"] } +hdrhistogram = "7" +itertools = "0.12" +rand = "0.8.5" +rand_mt = "4.2.1" +tempfile = "3" + +[features] +deadlock = ["parking_lot/deadlock_detection"] diff --git a/foyer-experimental/src/buf.rs b/foyer-experimental/src/buf.rs new file mode 100644 index 00000000..2366bf1f --- /dev/null +++ b/foyer-experimental/src/buf.rs @@ -0,0 +1,83 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bytes::{Buf, BufMut}; + +pub trait BufExt: Buf { + cfg_match! { + cfg(target_pointer_width = "16") => { + fn get_usize(&mut self) -> usize { + self.get_u16() as usize + } + + fn get_isize(&mut self) -> isize { + self.get_i16() as isize + } + } + cfg(target_pointer_width = "32") => { + fn get_usize(&mut self) -> usize { + self.get_u32() as usize + } + + fn get_isize(&mut self) -> isize { + self.get_i32() as isize + } + } + cfg(target_pointer_width = "64") => { + fn get_usize(&mut self) -> usize { + self.get_u64() as usize + } + + fn get_isize(&mut self) -> isize { + self.get_i64() as isize + } + } + } +} + +impl BufExt for T {} + +pub trait BufMutExt: BufMut { + cfg_match! { + cfg(target_pointer_width = "16") => { + fn put_usize(&mut self, v: usize) { + self.put_u16(v as u16); + } + + fn put_isize(&mut self, v: isize) { + self.put_i16(v as i16); + } + } + cfg(target_pointer_width = "32") => { + fn put_usize(&mut self, v: usize) { + self.put_u32(v as u32); + } + + fn put_isize(&mut self, v: isize) { + self.put_i32(v as i32); + } + } + cfg(target_pointer_width = "64") => { + fn put_usize(&mut self, v: usize) { + self.put_u64(v as u64); + } + + fn put_isize(&mut self, v: isize) { + self.put_i64(v as i64); + } + } + } +} + +impl BufMutExt for T {} diff --git a/foyer-experimental/src/error.rs b/foyer-experimental/src/error.rs new file mode 100644 index 00000000..383930e4 --- /dev/null +++ b/foyer-experimental/src/error.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[derive(thiserror::Error, Debug)] +#[error("{0}")] +pub struct Error(Box); + +#[derive(thiserror::Error, Debug)] +#[error("{source:?}")] +struct ErrorInner { + #[from] + source: ErrorKind, + // TODO(MrCroxx): Restore this after `error_generic_member_access` is stable. + // backtrace: Backtrace, +} + +#[derive(thiserror::Error, Debug)] +pub enum ErrorKind { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("other error: {0}")] + Other(#[from] anyhow::Error), +} + +impl From for Error { + fn from(value: ErrorKind) -> Self { + value.into() + } +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + value.into() + } +} + +impl From for Error { + fn from(value: anyhow::Error) -> Self { + value.into() + } +} + +pub type Result = core::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} diff --git a/foyer-experimental/src/lib.rs b/foyer-experimental/src/lib.rs new file mode 100644 index 00000000..c8a1dd61 --- /dev/null +++ b/foyer-experimental/src/lib.rs @@ -0,0 +1,38 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod error; +pub mod metrics; +pub mod notify; +pub mod wal; + +#[cfg(not(madsim))] +#[tracing::instrument(level = "trace", skip(f))] +async fn asyncify(f: F) -> T +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + tokio::task::spawn_blocking(f).await.unwrap() +} + +#[cfg(madsim)] +#[tracing::instrument(level = "trace", skip(f))] +async fn asyncify(f: F) -> T +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + f() +} diff --git a/foyer-experimental/src/metrics.rs b/foyer-experimental/src/metrics.rs new file mode 100644 index 00000000..8a15e4ec --- /dev/null +++ b/foyer-experimental/src/metrics.rs @@ -0,0 +1,194 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::OnceLock; + +use prometheus::{ + core::{AtomicU64, GenericGauge, GenericGaugeVec}, + exponential_buckets, opts, register_histogram_vec_with_registry, register_int_counter_vec_with_registry, Histogram, + HistogramVec, IntCounter, IntCounterVec, Registry, +}; + +type UintGaugeVec = GenericGaugeVec; +type UintGauge = GenericGauge; + +macro_rules! register_gauge_vec { + ($TYPE:ident, $OPTS:expr, $LABELS_NAMES:expr, $REGISTRY:expr $(,)?) => {{ + let gauge_vec = $TYPE::new($OPTS, $LABELS_NAMES).unwrap(); + $REGISTRY.register(Box::new(gauge_vec.clone())).map(|_| gauge_vec) + }}; +} + +macro_rules! register_uint_gauge_vec_with_registry { + ($OPTS:expr, $LABELS_NAMES:expr, $REGISTRY:expr $(,)?) => {{ + register_gauge_vec!(UintGaugeVec, $OPTS, $LABELS_NAMES, $REGISTRY) + }}; + + ($NAME:expr, $HELP:expr, $LABELS_NAMES:expr, $REGISTRY:expr $(,)?) => {{ + register_uint_gauge_vec_with_registry!(opts!($NAME, $HELP), $LABELS_NAMES, $REGISTRY) + }}; +} + +static REGISTRY: OnceLock = OnceLock::new(); + +/// Set metrics registry for `foyer`. +/// +/// Metrics registry must be set before `open`. +/// +/// Return `true` if set succeeds. +pub fn set_metrics_registry(registry: Registry) -> bool { + REGISTRY.set(registry).is_ok() +} + +pub fn get_metrics_registry() -> &'static Registry { + REGISTRY.get_or_init(|| prometheus::default_registry().clone()) +} + +// TODO(MrCroxx): Use `LazyLock` after `lazy_cell` is stable. +// pub static METRICS: LazyLock = LazyLock::new(GlobalMetrics::default); + +lazy_static::lazy_static! { + pub static ref METRICS: GlobalMetrics = GlobalMetrics::default(); +} + +#[derive(Debug)] +pub struct GlobalMetrics { + pub op_duration: HistogramVec, + pub op_bytes: IntCounterVec, + + pub total_bytes: UintGaugeVec, + + pub inner_op_duration: HistogramVec, + pub inner_op_bytes: IntCounterVec, + pub inner_op_bytes_distribution: HistogramVec, +} + +impl Default for GlobalMetrics { + fn default() -> Self { + Self::new(get_metrics_registry()) + } +} + +impl GlobalMetrics { + pub fn new(registry: &Registry) -> Self { + let op_duration = register_histogram_vec_with_registry!( + "foyer_storage_op_duration", + "foyer storage op duration", + &["foyer", "op", "extra"], + exponential_buckets(0.0001, 2.0, 16).unwrap(), // 100 us ~ 6.55 s + registry, + ) + .unwrap(); + let op_bytes = register_int_counter_vec_with_registry!( + "foyer_storage_op_bytes", + "foyer storage op bytes", + &["foyer", "op", "extra"], + registry, + ) + .unwrap(); + + let total_bytes = register_uint_gauge_vec_with_registry!( + "foyer_storage_total_bytes", + "foyer storage total bytes", + &["foyer", "total", "extra"], + registry, + ) + .unwrap(); + + let inner_op_duration = register_histogram_vec_with_registry!( + "foyer_storage_inner_op_duration", + "foyer storage inner op duration", + &["foyer", "op", "extra"], + exponential_buckets(0.0001, 2.0, 16).unwrap(), // 1 us ~ 0.655 s + registry, + ) + .unwrap(); + let inner_op_bytes = register_int_counter_vec_with_registry!( + "foyer_storage_inner_op_bytes", + "foyer storage inner op bytes", + &["foyer", "op", "extra"], + registry, + ) + .unwrap(); + let inner_op_bytes_distribution = register_histogram_vec_with_registry!( + "foyer_storage_inner_op_bytes_distribution", + "foyer storage inner op bytes distribution", + &["foyer", "op", "extra"], + exponential_buckets(0.0001, 2.0, 16).unwrap(), // 1 us ~ 0.655 s + registry, + ) + .unwrap(); + + Self { + op_duration, + op_bytes, + total_bytes, + inner_op_duration, + inner_op_bytes, + inner_op_bytes_distribution, + } + } + + pub fn metrics(&self, foyer: &str) -> Metrics { + Metrics::new(self, foyer) + } +} + +#[derive(Debug)] +pub struct Metrics { + pub inner_op_duration_wal_flush: Histogram, + pub inner_op_bytes_wal_flush: IntCounter, + pub inner_op_bytes_distribution_wal_flush: Histogram, + + pub inner_op_duration_wal_write: Histogram, + pub inner_op_duration_wal_sync: Histogram, + + pub inner_op_duration_wal_append: Histogram, + pub inner_op_duration_wal_notify: Histogram, + + pub _total_bytes: UintGauge, +} + +impl Metrics { + pub fn new(global: &GlobalMetrics, foyer: &str) -> Self { + let inner_op_duration_wal_flush = global.inner_op_duration.with_label_values(&[foyer, "wal", "flush"]); + let inner_op_bytes_wal_flush = global.inner_op_bytes.with_label_values(&[foyer, "wal", "flush"]); + let inner_op_bytes_distribution_wal_flush = global + .inner_op_bytes_distribution + .with_label_values(&[foyer, "wal", "flush"]); + + let inner_op_duration_wal_write = global.inner_op_duration.with_label_values(&[foyer, "wal", "write"]); + + let inner_op_duration_wal_sync = global.inner_op_duration.with_label_values(&[foyer, "wal", "sync"]); + + let inner_op_duration_wal_append = global.inner_op_duration.with_label_values(&[foyer, "wal", "append"]); + let inner_op_duration_wal_notify = global.inner_op_duration.with_label_values(&[foyer, "wal", "notify"]); + + let total_bytes = global.total_bytes.with_label_values(&[foyer, "", ""]); + + Self { + inner_op_duration_wal_flush, + inner_op_bytes_wal_flush, + inner_op_bytes_distribution_wal_flush, + + inner_op_duration_wal_write, + inner_op_duration_wal_sync, + + inner_op_duration_wal_append, + inner_op_duration_wal_notify, + + _total_bytes: total_bytes, + } + } +} diff --git a/foyer-experimental/src/notify.rs b/foyer-experimental/src/notify.rs new file mode 100644 index 00000000..e98a409e --- /dev/null +++ b/foyer-experimental/src/notify.rs @@ -0,0 +1,129 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, OnceLock, + }, + thread::{current, park, Thread}, +}; + +use crossbeam::utils::Backoff; + +/// A multi-producer-single-consumer blocking notify implementation. +#[derive(Debug, Default, Clone)] +pub struct Notify { + ready: Arc, + thread: Arc>, +} + +impl Notify { + pub fn notified(&self) -> Notified { + self.thread.set(current()).unwrap(); + Notified::new(self.ready.clone()) + } + + pub fn notify(&self) { + self.ready.store(true, Ordering::SeqCst); + self.thread.get().unwrap().unpark(); + } +} + +#[derive(Debug)] +pub struct Notified { + ready: Arc, + backoff: Backoff, +} + +impl Notified { + fn new(ready: Arc) -> Self { + Self { + ready, + backoff: Backoff::default(), + } + } + + pub fn wait(&self) { + while !self.ready.load(Ordering::SeqCst) { + if self.backoff.is_completed() { + park(); + } else { + self.backoff.snooze(); + } + } + self.ready.store(false, Ordering::SeqCst); + self.backoff.reset(); + } +} + +#[cfg(test)] +mod tests { + use std::{ + sync::Arc, + thread::{sleep, spawn}, + time::Duration, + }; + + use super::*; + + #[test] + fn assert_notify_send_sync_static() { + fn is_send_sync_static() {} + is_send_sync_static::() + } + + #[test] + fn test_notify_buffer_one() { + let notify = Notify::default(); + + let notified = notify.notified(); + + notify.notify(); + + notified.wait(); + } + + #[test] + fn test_notify_buffer_one_block() { + let handle = spawn(|| { + let notify = Notify::default(); + + let notified = notify.notified(); + + notify.notify(); + + notified.wait(); + notified.wait(); + }); + + sleep(Duration::from_secs(1)); + + assert!(!handle.is_finished()); + } + + #[test] + fn test_notify() { + let notify = Arc::new(Notify::default()); + let n = notify.clone(); + let handle = spawn(move || { + let notified = n.notified(); + + notified.wait(); + }); + sleep(Duration::from_secs(1)); + notify.notify(); + handle.join().unwrap(); + } +} diff --git a/foyer-experimental/src/wal.rs b/foyer-experimental/src/wal.rs new file mode 100644 index 00000000..7d2673f5 --- /dev/null +++ b/foyer-experimental/src/wal.rs @@ -0,0 +1,315 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fs::{File, OpenOptions}, + io::Write, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread::JoinHandle, +}; + +use bytes::{Buf, BufMut}; +use crossbeam::channel; +use foyer_common::code::{BufExt, BufMutExt}; +use parking_lot::{Condvar, Mutex}; +use tokio::sync::oneshot; + +use crate::{asyncify, error::Result, metrics::Metrics}; + +pub trait HashValue: Send + Sync + 'static + Eq + std::fmt::Debug { + fn size() -> usize; + fn write(&self, buf: impl BufMut); + fn read(buf: impl Buf) -> Self; +} + +macro_rules! for_all_primitives { + ($macro:ident) => { + $macro! { + u8, u16, u32, u64, usize, + i8, i16, i32, i64, isize, + } + }; +} + +macro_rules! impl_hash_value { + ($( $type:ty, )*) => { + paste::paste! { + $( + impl HashValue for $type { + fn size() -> usize { + std::mem::size_of::<$type>() + } + + fn write(&self, mut buf: impl BufMut) { + buf.[](*self); + } + + fn read(mut buf: impl Buf) -> Self { + buf.[]() + } + } + )* + } + }; +} + +for_all_primitives! { impl_hash_value } + +#[derive(Debug)] +pub struct Tombstone { + hash: H, + pos: u32, +} + +impl Tombstone { + pub fn size() -> usize { + H::size() + 8 + } + + pub fn new(hash: H, pos: u32) -> Self { + Self { hash, pos } + } + + pub fn write(&self, mut buf: impl BufMut) { + self.hash.write(&mut buf); + buf.put_u32(self.pos); + } + + pub fn read(mut buf: impl Buf) -> Self { + let hash = H::read(&mut buf); + let pos = buf.get_u32(); + Self { hash, pos } + } +} + +#[derive(Debug)] +pub struct TombstoneLogConfig { + pub id: usize, + pub dir: PathBuf, + pub metrics: Arc, +} + +#[derive(Debug)] +struct InflightTombstone { + tombstone: Tombstone, + tx: oneshot::Sender>, +} + +#[derive(Debug)] +struct TombstoneLogInner { + inflights: Mutex>>, + condvar: Condvar, +} + +#[derive(Debug)] +pub struct TombstoneLog { + inner: Arc>, + + metrics: Arc, + + stopped: Arc, + handles: Arc>>>>, +} + +impl Clone for TombstoneLog { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + metrics: self.metrics.clone(), + stopped: self.stopped.clone(), + handles: self.handles.clone(), + } + } +} + +impl TombstoneLog { + pub async fn open(config: TombstoneLogConfig) -> Result { + let mut path = config.dir; + + path.push(format!("tombstone-{:08X}", config.id)); + + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::suspicious_open_options)] + let file = OpenOptions::new().write(true).read(true).create(true).open(path)?; + + let inner = Arc::new(TombstoneLogInner { + inflights: Mutex::new(vec![]), + condvar: Condvar::new(), + }); + + let (task_tx, task_rx) = channel::unbounded(); + let stopped = Arc::new(AtomicBool::new(false)); + let metrics = config.metrics.clone(); + + let flusher = TombstoneLogFlusher { + file, + inner: inner.clone(), + task_tx, + metrics: metrics.clone(), + stopped: stopped.clone(), + }; + + let notifier = TombstoneLogFlushNotifier { + task_rx, + metrics: metrics.clone(), + }; + + let handles = vec![ + std::thread::spawn(move || flusher.run()), + std::thread::spawn(move || notifier.run()), + ]; + let handles = Arc::new(Mutex::new(handles)); + + Ok(Self { + inner, + + metrics, + + stopped, + handles, + }) + } + + pub async fn close(&self) -> Result<()> { + let handles = std::mem::take(&mut *self.handles.lock()); + if !handles.is_empty() { + self.stopped.store(true, Ordering::Release); + self.inner.condvar.notify_one(); + for handle in handles { + asyncify(move || handle.join().unwrap()).await?; + } + } + Ok(()) + } + + pub async fn append(&self, tombstone: Tombstone) -> Result<()> { + let timer = self.metrics.inner_op_duration_wal_append.start_timer(); + + let rx = { + let mut inflights = self.inner.inflights.lock(); + let (tx, rx) = oneshot::channel(); + inflights.push(InflightTombstone { tombstone, tx }); + rx + }; + self.inner.condvar.notify_one(); + let res = rx.await.unwrap(); + + drop(timer); + + res + } +} + +#[derive(Debug)] +struct TombstoneLogFlusher { + file: File, + + inner: Arc>, + + task_tx: channel::Sender, + + metrics: Arc, + + stopped: Arc, +} + +impl TombstoneLogFlusher { + fn run(mut self) -> Result<()> { + loop { + let inflights = { + let mut inflights = self.inner.inflights.lock(); + if self.stopped.load(Ordering::Relaxed) { + return Ok(()); + } + if inflights.is_empty() { + self.inner.condvar.wait_while(&mut inflights, |inflights| { + inflights.is_empty() && !self.stopped.load(Ordering::Relaxed) + }); + } + if self.stopped.load(Ordering::Relaxed) { + return Ok(()); + } + std::mem::take(&mut *inflights) + }; + + let timer = self.metrics.inner_op_duration_wal_flush.start_timer(); + + let mut buffer = Vec::with_capacity(Tombstone::::size() * inflights.len()); + let mut txs = Vec::with_capacity(inflights.len()); + + for inflight in inflights { + inflight.tombstone.write(&mut buffer); + txs.push(inflight.tx); + } + + let timer_write = self.metrics.inner_op_duration_wal_write.start_timer(); + match self.file.write_all(&buffer) { + Ok(()) => {} + Err(e) => { + self.task_tx.send(FlushNotifyTask { txs, io_result: Err(e) }).unwrap(); + continue; + } + } + drop(timer_write); + + let timer_sync = self.metrics.inner_op_duration_wal_sync.start_timer(); + match self.file.sync_data() { + Ok(()) => {} + Err(e) => { + self.task_tx.send(FlushNotifyTask { txs, io_result: Err(e) }).unwrap(); + continue; + } + } + drop(timer_sync); + + self.task_tx.send(FlushNotifyTask { txs, io_result: Ok(()) }).unwrap(); + + drop(timer); + } + } +} + +#[derive(Debug)] +struct FlushNotifyTask { + txs: Vec>>, + io_result: std::io::Result<()>, +} + +#[derive(Debug)] +struct TombstoneLogFlushNotifier { + task_rx: channel::Receiver, + + metrics: Arc, +} + +impl TombstoneLogFlushNotifier { + fn run(self) -> Result<()> { + while let Ok(task) = self.task_rx.recv() { + let timer = self.metrics.inner_op_duration_wal_notify.start_timer(); + for tx in task.txs { + let res = match &task.io_result { + Ok(()) => Ok(()), + Err(e) => Err(std::io::Error::from(e.kind()).into()), + }; + tx.send(res).unwrap(); + } + drop(timer); + } + Ok(()) + } +} diff --git a/foyer-intrusive/Cargo.toml b/foyer-intrusive/Cargo.toml new file mode 100644 index 00000000..f178e839 --- /dev/null +++ b/foyer-intrusive/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "foyer-intrusive" +version = "0.4.0" +edition = "2021" +authors = ["MrCroxx "] +description = "intrusive data structures for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +bytes = "1" +cmsketch = "0.1" +foyer-common = { version = "0.5", path = "../foyer-common" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +itertools = "0.12" +memoffset = "0.9" +parking_lot = "0.12" +paste = "1.0" +tracing = "0.1" +twox-hash = "1" diff --git a/foyer-intrusive/README.md b/foyer-intrusive/README.md new file mode 100644 index 00000000..5afa3268 --- /dev/null +++ b/foyer-intrusive/README.md @@ -0,0 +1,8 @@ +# foyer-intrusive + +Instrusive index collections and ordering collections for foyer. + +## Acknowledgments + +[facebook/CacheLib](https://github.com/facebook/CacheLib) +[Amanieu/intrusive-rs](https://github.com/Amanieu/intrusive-rs) \ No newline at end of file diff --git a/foyer-intrusive/src/collections/dlist.rs b/foyer-intrusive/src/collections/dlist.rs new file mode 100644 index 00000000..2712808d --- /dev/null +++ b/foyer-intrusive/src/collections/dlist.rs @@ -0,0 +1,640 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ptr::NonNull; + +use crate::core::{ + adapter::{Adapter, Link}, + pointer::Pointer, +}; + +#[derive(Debug, Default)] +pub struct DlistLink { + prev: Option>, + next: Option>, + is_linked: bool, +} + +impl DlistLink { + pub fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } + + pub fn prev(&self) -> Option> { + self.prev + } + + pub fn next(&self) -> Option> { + self.next + } +} + +unsafe impl Send for DlistLink {} +unsafe impl Sync for DlistLink {} + +impl Link for DlistLink { + fn is_linked(&self) -> bool { + self.is_linked + } +} + +#[derive(Debug)] +pub struct Dlist +where + A: Adapter, +{ + head: Option>, + tail: Option>, + + len: usize, + + adapter: A, +} + +impl Drop for Dlist +where + A: Adapter, +{ + fn drop(&mut self) { + let mut iter = self.iter_mut(); + iter.front(); + while iter.is_valid() { + iter.remove(); + } + assert!(self.is_empty()); + } +} + +impl Dlist +where + A: Adapter, +{ + pub fn new() -> Self { + Self { + head: None, + tail: None, + len: 0, + + adapter: A::new(), + } + } + + pub fn front(&self) -> Option<&::Item> { + unsafe { + self.head + .map(|link| self.adapter.link2item(link)) + .map(|link| link.as_ref()) + } + } + + pub fn back(&self) -> Option<&::Item> { + unsafe { + self.tail + .map(|link| self.adapter.link2item(link)) + .map(|link| link.as_ref()) + } + } + + pub fn front_mut(&mut self) -> Option<&mut ::Item> { + unsafe { + self.head + .map(|link| self.adapter.link2item(link)) + .map(|mut link| link.as_mut()) + } + } + + pub fn back_mut(&mut self) -> Option<&mut ::Item> { + unsafe { + self.tail + .map(|link| self.adapter.link2item(link)) + .map(|mut link| link.as_mut()) + } + } + + pub fn push_front(&mut self, ptr: A::Pointer) { + self.iter_mut().insert_after(ptr); + } + + pub fn push_back(&mut self, ptr: A::Pointer) { + self.iter_mut().insert_before(ptr); + } + + pub fn pop_front(&mut self) -> Option { + let mut iter = self.iter_mut(); + iter.next(); + iter.remove() + } + + pub fn pop_back(&mut self) -> Option { + let mut iter = self.iter_mut(); + iter.prev(); + iter.remove() + } + + pub fn iter(&self) -> DlistIter<'_, A> { + DlistIter { + link: None, + dlist: self, + } + } + + pub fn iter_mut(&mut self) -> DlistIterMut<'_, A> { + DlistIterMut { + link: None, + dlist: self, + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get the prev element of the given `link`. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn prev_of_raw(&self, link: NonNull) -> Option<&::Item> { + link.as_ref().prev().map(|link| self.adapter.link2item(link).as_ref()) + } + + /// Get the mutable prev element of the given `link`. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn prev_mut_of_raw(&self, link: NonNull) -> Option<&mut ::Item> { + link.as_ref().prev().map(|link| self.adapter.link2item(link).as_mut()) + } + + /// Get the next element of the given `link`. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn next_of_raw(&self, link: NonNull) -> Option<&::Item> { + link.as_ref().next().map(|link| self.adapter.link2item(link).as_ref()) + } + + /// Get the mutable next element of the given `link`. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn next_mut_of_raw(&self, link: NonNull) -> Option<&mut ::Item> { + link.as_ref().next().map(|link| self.adapter.link2item(link).as_mut()) + } + + /// Remove an node that holds the given raw link. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn remove_raw(&mut self, link: NonNull) -> A::Pointer { + let mut iter = self.iter_mut_from_raw(link); + debug_assert!(iter.is_valid()); + iter.remove().unwrap_unchecked() + } + + /// Create mutable iterator directly on raw link. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn iter_mut_from_raw(&mut self, link: NonNull) -> DlistIterMut<'_, A> { + DlistIterMut { + link: Some(link), + dlist: self, + } + } + + /// Create immutable iterator directly on raw link. + /// + /// # Safety + /// + /// `link` MUST be in this [`Dlist`]. + pub unsafe fn iter_from_raw(&self, link: NonNull) -> DlistIter<'_, A> { + DlistIter { + link: Some(link), + dlist: self, + } + } + + /// # Safety + /// + /// `self` must be empty. `src` will be set empty after operation. + pub unsafe fn replace_with(&mut self, src: &mut Dlist) { + debug_assert!(self.head.is_none()); + debug_assert!(self.tail.is_none()); + debug_assert_eq!(self.len, 0); + + self.head = src.head; + self.tail = src.tail; + self.len = src.len; + + src.head = None; + src.tail = None; + src.len = 0; + } +} + +pub struct DlistIter<'a, A> +where + A: Adapter, +{ + link: Option>, + dlist: &'a Dlist, +} + +impl<'a, A> DlistIter<'a, A> +where + A: Adapter, +{ + pub fn is_valid(&self) -> bool { + self.link.is_some() + } + + pub fn get(&self) -> Option<&::Item> { + self.link + .map(|link| unsafe { self.dlist.adapter.link2item(link).as_ref() }) + } + + /// Move to next. + /// + /// If iter is on tail, move to null. + /// If iter is on null, move to head. + pub fn next(&mut self) { + unsafe { + match self.link { + Some(link) => self.link = link.as_ref().next, + None => self.link = self.dlist.head, + } + } + } + + /// Move to prev. + /// + /// If iter is on head, move to null. + /// If iter is on null, move to tail. + pub fn prev(&mut self) { + unsafe { + match self.link { + Some(link) => self.link = link.as_ref().prev, + None => self.link = self.dlist.tail, + } + } + } + + /// Move to head. + pub fn front(&mut self) { + self.link = self.dlist.head; + } + + /// Move to head. + pub fn back(&mut self) { + self.link = self.dlist.tail; + } + + pub fn is_front(&self) -> bool { + self.link == self.dlist.head + } + + pub fn is_back(&self) -> bool { + self.link == self.dlist.tail + } +} + +pub struct DlistIterMut<'a, A> +where + A: Adapter, +{ + link: Option>, + dlist: &'a mut Dlist, +} + +impl<'a, A> DlistIterMut<'a, A> +where + A: Adapter, +{ + pub fn is_valid(&self) -> bool { + self.link.is_some() + } + + pub fn get(&self) -> Option<&::Item> { + self.link + .map(|link| unsafe { self.dlist.adapter.link2item(link).as_ref() }) + } + + pub fn get_mut(&mut self) -> Option<&mut ::Item> { + self.link + .map(|link| unsafe { self.dlist.adapter.link2item(link).as_mut() }) + } + + /// Move to next. + /// + /// If iter is on tail, move to null. + /// If iter is on null, move to head. + pub fn next(&mut self) { + unsafe { + match self.link { + Some(link) => self.link = link.as_ref().next, + None => self.link = self.dlist.head, + } + } + } + + /// Move to prev. + /// + /// If iter is on head, move to null. + /// If iter is on null, move to tail. + pub fn prev(&mut self) { + unsafe { + match self.link { + Some(link) => self.link = link.as_ref().prev, + None => self.link = self.dlist.tail, + } + } + } + + /// Move to front. + pub fn front(&mut self) { + self.link = self.dlist.head; + } + + /// Move to back. + pub fn back(&mut self) { + self.link = self.dlist.tail; + } + + /// Removes the current item from [`Dlist`] and move next. + pub fn remove(&mut self) -> Option { + unsafe { + if !self.is_valid() { + return None; + } + + debug_assert!(self.is_valid()); + let mut link = self.link.unwrap_unchecked(); + + let item = self.dlist.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + + // fix head and tail if node is either of that + let mut prev = link.as_ref().prev; + let mut next = link.as_ref().next; + if Some(link) == self.dlist.head { + self.dlist.head = next; + } + if Some(link) == self.dlist.tail { + self.dlist.tail = prev; + } + + // fix the next and prev ptrs of the node before and after this + if let Some(prev) = &mut prev { + prev.as_mut().next = next; + } + if let Some(next) = &mut next { + next.as_mut().prev = prev; + } + + link.as_mut().next = None; + link.as_mut().prev = None; + link.as_mut().is_linked = false; + + self.dlist.len -= 1; + + self.link = next; + + Some(ptr) + } + } + + /// Link a new ptr before the current one. + /// + /// If iter is on null, link to tail. + pub fn insert_before(&mut self, ptr: A::Pointer) { + unsafe { + let item_new = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let mut link_new = self.dlist.adapter.item2link(item_new); + assert!(!link_new.as_ref().is_linked()); + + match self.link { + Some(link) => self.link_before(link_new, link), + None => { + self.link_between(link_new, self.dlist.tail, None); + self.dlist.tail = Some(link_new); + } + } + + if self.dlist.head == self.link { + self.dlist.head = Some(link_new); + } + + link_new.as_mut().is_linked = true; + + self.dlist.len += 1; + } + } + + /// Link a new ptr after the current one. + /// + /// If iter is on null, link to head. + pub fn insert_after(&mut self, ptr: A::Pointer) { + unsafe { + let item_new = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let mut link_new = self.dlist.adapter.item2link(item_new); + assert!(!link_new.as_ref().is_linked()); + + match self.link { + Some(link) => self.link_after(link_new, link), + None => { + self.link_between(link_new, None, self.dlist.head); + self.dlist.head = Some(link_new); + } + } + + if self.dlist.tail == self.link { + self.dlist.tail = Some(link_new); + } + + link_new.as_mut().is_linked = true; + + self.dlist.len += 1; + } + } + + unsafe fn link_before(&mut self, link: NonNull, next: NonNull) { + self.link_between(link, next.as_ref().prev, Some(next)); + } + + unsafe fn link_after(&mut self, link: NonNull, prev: NonNull) { + self.link_between(link, Some(prev), prev.as_ref().next); + } + + unsafe fn link_between( + &mut self, + mut link: NonNull, + mut prev: Option>, + mut next: Option>, + ) { + if let Some(prev) = &mut prev { + prev.as_mut().next = Some(link); + } + if let Some(next) = &mut next { + next.as_mut().prev = Some(link); + } + link.as_mut().prev = prev; + link.as_mut().next = next; + } + + pub fn is_front(&self) -> bool { + self.link == self.dlist.head + } + + pub fn is_back(&self) -> bool { + self.link == self.dlist.tail + } +} + +impl<'a, A> Iterator for DlistIter<'a, A> +where + A: Adapter, +{ + type Item = &'a ::Item; + + fn next(&mut self) -> Option { + self.next(); + match self.link { + Some(link) => Some(unsafe { self.dlist.adapter.link2item(link).as_ref() }), + None => None, + } + } +} + +impl<'a, A> Iterator for DlistIterMut<'a, A> +where + A: Adapter, +{ + type Item = &'a mut ::Item; + + fn next(&mut self) -> Option { + self.next(); + match self.link { + Some(link) => Some(unsafe { self.dlist.adapter.link2item(link).as_mut() }), + None => None, + } + } +} + +// TODO(MrCroxx): Need more tests. + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::intrusive_adapter; + + #[derive(Debug)] + struct DlistItem { + link: DlistLink, + val: u64, + } + + impl DlistItem { + fn new(val: u64) -> Self { + Self { + link: DlistLink::default(), + val, + } + } + } + + #[derive(Debug, Default)] + struct DlistAdapter; + + unsafe impl Adapter for DlistAdapter { + type Pointer = Box; + + type Link = DlistLink; + + fn new() -> Self { + Self + } + + unsafe fn link2item(&self, link: NonNull) -> NonNull<::Item> { + NonNull::new_unchecked(crate::container_of!(link.as_ptr(), DlistItem, link) as *mut _) + } + + unsafe fn item2link(&self, item: NonNull<::Item>) -> NonNull { + NonNull::new_unchecked((item.as_ptr() as *const u8).add(crate::offset_of!(DlistItem, link)) as *mut _) + } + } + + intrusive_adapter! { DlistArcAdapter = Arc: DlistItem { link: DlistLink } } + + #[test] + fn test_dlist_simple() { + let mut l = Dlist::::new(); + + l.push_back(Box::new(DlistItem::new(2))); + l.push_front(Box::new(DlistItem::new(1))); + l.push_back(Box::new(DlistItem::new(3))); + + let v = l.iter_mut().map(|item| item.val).collect_vec(); + assert_eq!(v, vec![1, 2, 3]); + assert_eq!(l.len(), 3); + + let mut iter = l.iter_mut(); + iter.next(); + iter.next(); + assert_eq!(iter.get().unwrap().val, 2); + let i2 = iter.remove(); + assert_eq!(i2.unwrap().val, 2); + assert_eq!(iter.get().unwrap().val, 3); + let v = l.iter_mut().map(|item| item.val).collect_vec(); + assert_eq!(v, vec![1, 3]); + assert_eq!(l.len(), 2); + + let i3 = l.pop_back(); + assert_eq!(i3.unwrap().val, 3); + let i1 = l.pop_front(); + assert_eq!(i1.unwrap().val, 1); + assert!(l.pop_front().is_none()); + assert_eq!(l.len(), 0); + } + + #[test] + fn test_arc_drop() { + let mut l = Dlist::::new(); + + let items = (0..10).map(|i| Arc::new(DlistItem::new(i))).collect_vec(); + for item in items.iter() { + l.push_back(item.clone()); + } + for item in items.iter() { + assert_eq!(Arc::strong_count(item), 2); + } + drop(l); + for item in items.iter() { + assert_eq!(Arc::strong_count(item), 1); + } + } +} diff --git a/foyer-intrusive/src/collections/duplicated_hashmap.rs b/foyer-intrusive/src/collections/duplicated_hashmap.rs new file mode 100644 index 00000000..7b3da1e7 --- /dev/null +++ b/foyer-intrusive/src/collections/duplicated_hashmap.rs @@ -0,0 +1,450 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, hash::Hasher, marker::PhantomData, ptr::NonNull}; + +use foyer_common::code::{Key, Value}; +use twox_hash::XxHash64; + +use super::dlist::{Dlist, DlistIter, DlistIterMut, DlistLink}; +use crate::{ + core::{ + adapter::{Adapter, KeyAdapter, Link}, + pointer::Pointer, + }, + intrusive_adapter, +}; + +pub struct DuplicatedHashMapLink { + slot_link: DlistLink, + group_link: DlistLink, + group: Dlist, +} + +impl Default for DuplicatedHashMapLink { + fn default() -> Self { + Self { + slot_link: DlistLink::default(), + group_link: DlistLink::default(), + group: Dlist::new(), + } + } +} + +impl Debug for DuplicatedHashMapLink { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DuplicatedHashMapLink") + .field("slot_link", &self.slot_link) + .field("group_link", &self.group_link) + .finish() + } +} + +intrusive_adapter! { DuplicatedHashMapLinkSlotAdapter = NonNull: DuplicatedHashMapLink { slot_link: DlistLink } } +intrusive_adapter! { DuplicatedHashMapLinkGroupAdapter = NonNull: DuplicatedHashMapLink { group_link: DlistLink } } + +impl DuplicatedHashMapLink { + pub fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } +} + +unsafe impl Send for DuplicatedHashMapLink {} +unsafe impl Sync for DuplicatedHashMapLink {} + +impl Link for DuplicatedHashMapLink { + fn is_linked(&self) -> bool { + self.group_link.is_linked() + } +} + +pub struct DuplicatedHashMap +where + K: Key, + V: Value, + A: KeyAdapter, +{ + slots: Vec>, + + len: usize, + + adapter: A, + + _marker: PhantomData, +} + +impl Drop for DuplicatedHashMap +where + K: Key, + V: Value, + A: KeyAdapter, +{ + fn drop(&mut self) { + unsafe { + for slot in self.slots.iter_mut() { + let mut iter_slot = slot.iter_mut(); + iter_slot.front(); + while iter_slot.is_valid() { + let mut link_slot = iter_slot.remove().unwrap(); + let mut iter_group = link_slot.as_mut().group.iter_mut(); + iter_group.front(); + while iter_group.is_valid() { + let link_group = iter_group.remove().unwrap(); + let item = self.adapter.link2item(link_group); + let _ = A::Pointer::from_ptr(item.as_ptr()); + } + } + } + } + } +} + +impl DuplicatedHashMap +where + K: Key, + V: Value, + A: KeyAdapter, +{ + pub fn new(bits: usize) -> Self { + let mut slots = Vec::with_capacity(1 << bits); + for _ in 0..(1 << bits) { + slots.push(Dlist::new()); + } + Self { + slots, + len: 0, + adapter: A::new(), + _marker: PhantomData, + } + } + + pub fn insert(&mut self, ptr: A::Pointer) { + unsafe { + let item_new = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let mut link_new = self.adapter.item2link(item_new); + + assert!(link_new.as_ref().group.is_empty()); + + let key_new = self.adapter.item2key(item_new).as_ref(); + let hash = self.hash_key(key_new); + let slot = (self.slots.len() - 1) & hash as usize; + + match self.lookup_inner_mut(key_new, slot) { + Some(mut iter) => { + let link = iter.get_mut().unwrap(); + link.group.push_back(link_new); + } + None => { + self.slots[slot].push_front(link_new); + link_new.as_mut().group.push_back(link_new); + } + } + + self.len += 1; + } + } + + pub fn remove(&mut self, key: &K) -> Vec { + unsafe { + let hash = self.hash_key(key); + let slot = (self.slots.len() - 1) & hash as usize; + + match self.lookup_inner_mut(key, slot) { + Some(mut iter) => { + let mut link = iter.remove().unwrap(); + let mut res = Vec::with_capacity(link.as_ref().group.len()); + let mut iter = link.as_mut().group.iter_mut(); + iter.front(); + while iter.is_valid() { + let link = iter.remove().unwrap(); + debug_assert!(!link.as_ref().is_linked()); + let item = self.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + res.push(ptr); + } + debug_assert!(link.as_ref().group.is_empty()); + self.len -= res.len(); + res + } + None => vec![], + } + } + } + + pub fn lookup(&self, key: &K) -> Vec<&::Item> { + unsafe { + let hash = self.hash_key(key); + let slot = (self.slots.len() - 1) & hash as usize; + + match self.lookup_inner(key, slot) { + Some(iter) => { + let link = iter.get().unwrap(); + let mut res = Vec::with_capacity(link.group.len()); + let mut iter = link.group.iter(); + iter.front(); + while iter.is_valid() { + let link = iter.get().unwrap(); + let item = self.adapter.link2item(link.raw()).as_ref(); + res.push(item); + iter.next(); + } + res + } + None => vec![], + } + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// # Safety + /// + /// `link` MUST be in this [`HashMap`]. + pub unsafe fn remove_in_place(&mut self, mut link: NonNull) -> A::Pointer { + assert!(link.as_ref().is_linked()); + let item = self.adapter.link2item(link); + let key = self.adapter.item2key(item).as_ref(); + let hash = self.hash_key(key); + let slot = (self.slots.len() - 1) & hash as usize; + + if link.as_ref().slot_link.is_linked() { + // the removed item is the group header + + // remove from slot list + self.slots[slot] + .iter_mut_from_raw(link.as_ref().slot_link.raw()) + .remove(); + + // remove from group list + link.as_mut() + .group + .iter_mut_from_raw(link.as_ref().group_link.raw()) + .remove(); + + // transfer group list and relink slot list if necessary + if let Some(header_new) = link.as_mut().group.front_mut() { + self.slots[slot].push_front(header_new.raw()); + header_new.group.replace_with(&mut link.as_mut().group); + } + } else { + // the removed item is NOT the group header + + // find header link + let header_link = { + let mut header = &link.as_ref().group_link; + while header.prev().is_some() { + header = &*header.prev().unwrap().as_ptr(); + } + let adapter = DuplicatedHashMapLinkGroupAdapter::new(); + adapter.link2item(header.raw()).as_mut() + }; + debug_assert!(header_link.slot_link.is_linked()); + + // remove from group link + header_link + .group + .iter_mut_from_raw(link.as_ref().group_link.raw()) + .remove(); + } + + self.len -= 1; + + debug_assert!(!link.as_ref().slot_link.is_linked()); + debug_assert!(!link.as_ref().group_link.is_linked()); + debug_assert!(link.as_ref().group.is_empty()); + + A::Pointer::from_ptr(item.as_ptr()) + } + + /// # Safety + /// + /// there must be at most one matches in the slot + unsafe fn lookup_inner_mut( + &mut self, + key: &K, + slot: usize, + ) -> Option> { + let mut iter = self.slots[slot].iter_mut(); + iter.front(); + while iter.is_valid() { + let item = self.adapter.link2item(iter.get().unwrap().raw()); + let ikey = self.adapter.item2key(item).as_ref(); + if ikey == key { + return Some(iter); + } + iter.next(); + } + None + } + + /// # Safety + /// + /// there must be at most one matches in the slot + unsafe fn lookup_inner(&self, key: &K, slot: usize) -> Option> { + let mut iter = self.slots[slot].iter(); + iter.front(); + while iter.is_valid() { + let item = self.adapter.link2item(iter.get().unwrap().raw()); + let ikey = self.adapter.item2key(item).as_ref(); + if ikey == key { + return Some(iter); + } + iter.next(); + } + None + } + + fn hash_key(&self, key: &K) -> u64 { + let mut hasher = XxHash64::default(); + key.hash(&mut hasher); + hasher.finish() + } +} + +// TODO(MrCroxx): Need more tests. + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::{intrusive_adapter, key_adapter}; + + #[derive(Debug)] + struct DuplicatedHashMapItem { + link: DuplicatedHashMapLink, + + key: u64, + value: u64, + } + + intrusive_adapter! { HashMapItemAdapter = Arc: DuplicatedHashMapItem { link: DuplicatedHashMapLink } } + key_adapter! { HashMapItemAdapter = DuplicatedHashMapItem { key: u64 } } + + #[test] + fn test_duplicated_hashmap_simple() { + // 2 slots + let mut map = DuplicatedHashMap::::new(1); + + // 8 * 8 items + let items = (0..8) + .map(|key| { + (0..8) + .map(|value| { + Arc::new(DuplicatedHashMapItem { + link: DuplicatedHashMapLink::default(), + key, + value, + }) + }) + .collect_vec() + }) + .collect_vec(); + + // 8 * 8 + for item in items.iter().flatten() { + map.insert(item.clone()); + } + for item in items.iter().flatten() { + assert_eq!(Arc::strong_count(item), 2); + } + for key in 0..8 { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + assert_eq!(vs, (0..8).collect_vec()); + } + + // { 0 2 4 6 } * 8 + for key in (0..8).skip(1).step_by(2) { + let res = map.remove(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + assert_eq!(vs, (0..8).collect_vec()); + } + for key in (0..8).step_by(2) { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + assert_eq!(vs, (0..8).collect_vec()); + } + + // 8 * 8 + for item in items.iter().skip(1).step_by(2).flatten() { + map.insert(item.clone()); + } + for key in 0..8 { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + assert_eq!(vs, (0..8).collect_vec()); + } + + // remove group member in place + unsafe { map.remove_in_place(items[4][4].link.raw()) }; + for key in 0..8 { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + if key == 4 { + assert_eq!(vs, vec![0, 1, 2, 3, 5, 6, 7]) + } else { + assert_eq!(vs, (0..8).collect_vec()); + } + } + // 8 * 8 + map.insert(items[4][4].clone()); + for key in 0..8 { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + assert_eq!(vs, (0..8).collect_vec()); + } + + // remove group head in place + unsafe { map.remove_in_place(items[4][0].link.raw()) }; + for key in 0..8 { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + if key == 4 { + assert_eq!(vs, vec![1, 2, 3, 4, 5, 6, 7]) + } else { + assert_eq!(vs, (0..8).collect_vec()); + } + } + // 8 * 8 + map.insert(items[4][0].clone()); + for key in 0..8 { + let res = map.lookup(&key); + let mut vs = res.iter().map(|item| item.value).collect_vec(); + vs.sort(); + assert_eq!(vs, (0..8).collect_vec()); + } + + drop(map); + + for item in items.into_iter().flatten() { + assert_eq!(Arc::strong_count(&item), 1); + } + } +} diff --git a/foyer-intrusive/src/collections/hashmap.rs b/foyer-intrusive/src/collections/hashmap.rs new file mode 100644 index 00000000..ff251070 --- /dev/null +++ b/foyer-intrusive/src/collections/hashmap.rs @@ -0,0 +1,413 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{hash::Hasher, marker::PhantomData, ptr::NonNull}; + +use foyer_common::code::{Key, Value}; +use twox_hash::XxHash64; + +use super::dlist::{Dlist, DlistIter, DlistIterMut, DlistLink}; +use crate::{ + core::{ + adapter::{KeyAdapter, Link}, + pointer::Pointer, + }, + intrusive_adapter, +}; + +#[derive(Debug, Default)] +pub struct HashMapLink { + dlist_link: DlistLink, +} + +intrusive_adapter! { HashMapLinkAdapter = NonNull: HashMapLink { dlist_link: DlistLink } } + +impl HashMapLink { + pub fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } +} + +unsafe impl Send for HashMapLink {} +unsafe impl Sync for HashMapLink {} + +impl Link for HashMapLink { + fn is_linked(&self) -> bool { + self.dlist_link.is_linked() + } +} + +pub struct HashMap +where + K: Key, + V: Value, + A: KeyAdapter, +{ + slots: Vec>, + + len: usize, + + adapter: A, + + _marker: PhantomData, +} + +impl Drop for HashMap +where + K: Key, + V: Value, + A: KeyAdapter, +{ + fn drop(&mut self) { + unsafe { + for slot in self.slots.iter_mut() { + let mut iter = slot.iter_mut(); + iter.front(); + while iter.is_valid() { + let link = iter.remove().unwrap(); + let item = self.adapter.link2item(link); + let _ = A::Pointer::from_ptr(item.as_ptr()); + } + } + } + } +} + +impl HashMap +where + K: Key, + V: Value, + A: KeyAdapter, +{ + pub fn new(bits: usize) -> Self { + let mut slots = Vec::with_capacity(1 << bits); + for _ in 0..(1 << bits) { + slots.push(Dlist::new()); + } + Self { + slots, + len: 0, + adapter: A::new(), + _marker: PhantomData, + } + } + + pub fn insert(&mut self, ptr: A::Pointer) -> Option { + unsafe { + let item_new = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let link_new = self.adapter.item2link(item_new); + + let key_new = self.adapter.item2key(item_new).as_ref(); + let hash = self.hash_key(key_new); + let slot = (self.slots.len() - 1) & hash as usize; + + let res = self.remove_inner(key_new, slot); + if res.is_some() { + self.len -= 1; + } + + self.slots[slot].push_front(link_new); + + self.len += 1; + + res + } + } + + pub fn remove(&mut self, key: &K) -> Option { + unsafe { + let hash = self.hash_key(key); + let slot = (self.slots.len() - 1) & hash as usize; + + let res = self.remove_inner(key, slot); + if res.is_some() { + self.len -= 1; + } + res + } + } + + pub fn lookup(&self, key: &K) -> Option<&::Item> { + unsafe { + let hash = self.hash_key(key); + let slot = (self.slots.len() - 1) & hash as usize; + + match self.lookup_inner(key, slot) { + Some(iter) => { + let link = iter.get().unwrap().raw(); + let item = self.adapter.link2item(link).as_ref(); + Some(item) + } + None => None, + } + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> HashMapIter<'_, K, V, A> { + HashMapIter::new(self) + } + + /// # Safety + /// + /// `link` MUST be in this [`HashMap`]. + pub unsafe fn remove_in_place(&mut self, link: NonNull) -> A::Pointer { + assert!(link.as_ref().is_linked()); + let item = self.adapter.link2item(link); + let key = self.adapter.item2key(item).as_ref(); + let hash = self.hash_key(key); + let slot = (self.slots.len() - 1) & hash as usize; + self.slots[slot] + .iter_mut_from_raw(link.as_ref().dlist_link.raw()) + .remove(); + self.len -= 1; + A::Pointer::from_ptr(item.as_ptr()) + } + + /// # Safety + /// + /// there must be at most one matches in the slot + unsafe fn lookup_inner_mut(&mut self, key: &K, slot: usize) -> Option> { + let mut iter = self.slots[slot].iter_mut(); + iter.front(); + while iter.is_valid() { + let item = self.adapter.link2item(iter.get().unwrap().raw()); + let ikey = self.adapter.item2key(item).as_ref(); + if ikey == key { + return Some(iter); + } + iter.next(); + } + None + } + + /// # Safety + /// + /// there must be at most one matches in the slot + unsafe fn lookup_inner(&self, key: &K, slot: usize) -> Option> { + let mut iter = self.slots[slot].iter(); + iter.front(); + while iter.is_valid() { + let item = self.adapter.link2item(iter.get().unwrap().raw()); + let ikey = self.adapter.item2key(item).as_ref(); + if ikey == key { + return Some(iter); + } + iter.next(); + } + None + } + + /// # Safety + /// + /// there must be at most one matches in the slot + unsafe fn remove_inner(&mut self, key: &K, slot: usize) -> Option { + match self.lookup_inner_mut(key, slot) { + Some(mut iter) => { + let link = iter.remove().unwrap(); + let item = self.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + Some(ptr) + } + None => None, + } + } + + fn hash_key(&self, key: &K) -> u64 { + let mut hasher = XxHash64::default(); + key.hash(&mut hasher); + hasher.finish() + } +} + +pub struct HashMapIter<'a, K, V, A> +where + K: Key, + V: Value, + A: KeyAdapter, +{ + slot: usize, + iters: Vec>, + + adapter: A, + _marker: PhantomData<(K, V)>, +} + +impl<'a, K, V, A> HashMapIter<'a, K, V, A> +where + K: Key, + V: Value, + A: KeyAdapter, +{ + pub fn new(hashmap: &'a HashMap) -> Self { + let mut iters = Vec::with_capacity(hashmap.slots.len()); + for slot in hashmap.slots.iter() { + let iter = slot.iter(); + iters.push(iter); + } + Self { + slot: 0, + iters, + + adapter: A::new(), + _marker: PhantomData, + } + } + + pub fn is_valid(&self) -> bool { + self.iters[self.slot].is_valid() + } + + pub fn get(&self) -> Option<&::Item> { + self.iters[self.slot] + .get() + .map(|link| unsafe { self.adapter.link2item(link.raw()).as_ref() }) + } + + pub fn get_mut(&mut self) -> Option<&mut ::Item> { + self.iters[self.slot] + .get() + .map(|link| unsafe { self.adapter.link2item(link.raw()).as_mut() }) + } + + /// Move to next. + /// + /// If iter is on tail, move to null. + /// If iter is on null, move to head. + pub fn next(&mut self) { + if self.iters[self.slot].is_valid() { + self.iters[self.slot].next(); + if !self.iters[self.slot].is_valid() && self.slot + 1 < self.iters.len() { + self.slot += 1; + self.iters[self.slot].front(); + debug_assert!(self.is_valid()); + } + } else { + self.front(); + } + } + + /// Move to prev. + /// + /// If iter is on head, move to null. + /// If iter is on null, move to tail. + pub fn prev(&mut self) { + if self.iters[self.slot].is_valid() { + self.iters[self.slot].prev(); + if !self.iters[self.slot].is_valid() && self.slot > 0 { + self.slot -= 1; + self.iters[self.slot].back(); + debug_assert!(self.is_valid()); + } + } else { + self.back(); + } + } + + /// Move to front. + pub fn front(&mut self) { + self.slot = 0; + self.iters[self.slot].front(); + } + + /// Move to back. + pub fn back(&mut self) { + self.slot = self.iters.len() - 1; + self.iters[self.slot].back(); + } +} + +// TODO(MrCroxx): Need more tests. + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::{intrusive_adapter, key_adapter}; + + #[derive(Debug)] + struct HashMapItem { + link: HashMapLink, + + key: u64, + value: u64, + } + + intrusive_adapter! { HashMapItemAdapter = Arc: HashMapItem { link: HashMapLink } } + key_adapter! { HashMapItemAdapter = HashMapItem { key: u64 } } + + #[test] + fn test_hashmap_simple() { + let mut map = HashMap::::new(6); + let items = (0..128) + .map(|i| HashMapItem { + link: HashMapLink::default(), + key: i, + value: i, + }) + .map(Arc::new) + .collect_vec(); + + for item in items.iter() { + map.insert(item.clone()); + } + + for i in 0..128 { + let item = map.lookup(&i).unwrap(); + assert_eq!(item.key, i); + assert_eq!(item.value, i); + } + + for i in 0..64 { + assert!(map.remove(&i).is_some()); + } + + for i in 0..64 { + assert!(map.lookup(&i).is_none()); + } + for i in 64..128 { + let item = map.lookup(&i).unwrap(); + assert_eq!(item.key, i); + assert_eq!(item.value, i); + } + + for item in items.iter() { + map.insert(item.clone()); + } + for i in 0..128 { + let item = map.lookup(&i).unwrap(); + assert_eq!(item.key, i); + assert_eq!(item.value, i); + } + + unsafe { map.remove_in_place(items[0].link.raw()) }; + assert!(map.lookup(&0).is_none()); + + drop(map); + + for item in items { + assert_eq!(Arc::strong_count(&item), 1); + } + } +} diff --git a/foyer-intrusive/src/collections/mod.rs b/foyer-intrusive/src/collections/mod.rs new file mode 100644 index 00000000..a43fff09 --- /dev/null +++ b/foyer-intrusive/src/collections/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod dlist; +pub mod duplicated_hashmap; +pub mod hashmap; diff --git a/foyer-intrusive/src/core/adapter.rs b/foyer-intrusive/src/core/adapter.rs new file mode 100644 index 00000000..eb09afbd --- /dev/null +++ b/foyer-intrusive/src/core/adapter.rs @@ -0,0 +1,367 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2020 Amari Robinson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; + +use foyer_common::code::Key; + +use crate::core::pointer::Pointer; + +pub trait Link: Send + Sync + 'static + Default + Debug { + fn is_linked(&self) -> bool; +} + +/// # Safety +/// +/// Pointer operations MUST be valid. +/// +/// [`Adapter`] is recommended to be generated by macro `instrusive_adapter!`. +pub unsafe trait Adapter: Send + Sync + Debug + 'static { + type Pointer: Pointer; + type Link: Link; + + fn new() -> Self; + + /// # Safety + /// + /// Pointer operations MUST be valid. + unsafe fn link2item( + &self, + link: std::ptr::NonNull, + ) -> std::ptr::NonNull<::Item>; + + /// # Safety + /// + /// Pointer operations MUST be valid. + unsafe fn item2link( + &self, + item: std::ptr::NonNull<::Item>, + ) -> std::ptr::NonNull; +} + +/// # Safety +/// +/// Pointer operations MUST be valid. +/// +/// [`KeyAdapter`] is recommended to be generated by macro `key_adapter!`. +pub unsafe trait KeyAdapter: Adapter { + type Key: Key; + + /// # Safety + /// + /// Pointer operations MUST be valid. + unsafe fn item2key( + &self, + item: std::ptr::NonNull<::Item>, + ) -> std::ptr::NonNull; +} + +/// # Safety +/// +/// Pointer operations MUST be valid. +/// +/// [`PriorityAdapter`] is recommended to be generated by macro `priority_adapter!`. +pub unsafe trait PriorityAdapter: Adapter { + type Priority: PartialEq + Eq + PartialOrd + Ord + Clone + Copy + Into; + + /// # Safety + /// + /// Pointer operations MUST be valid. + unsafe fn item2priority( + &self, + item: std::ptr::NonNull<::Item>, + ) -> std::ptr::NonNull; +} + +/// Macro to generate an implementation of [`Adapter`] for instrusive container and items. +/// +/// The basic syntax to create an adapter is: +/// +/// ```rust,ignore +/// intrusive_adapter! { Adapter = Pointer: Item { link_field: LinkType } } +/// ``` +/// +/// # Generics +/// +/// This macro supports generic arguments: +/// +/// Note that due to macro parsing limitations, `T: Trait` bounds are not +/// supported in the generic argument list. You must list any trait bounds in +/// a separate `where` clause at the end of the macro. +/// +/// # Examples +/// +/// ``` +/// use foyer_intrusive::{intrusive_adapter, key_adapter}; +/// use foyer_intrusive::core::adapter::{Adapter, KeyAdapter, Link}; +/// use foyer_intrusive::core::pointer::Pointer; +/// use foyer_intrusive::eviction::EvictionPolicy; +/// use std::sync::Arc; +/// +/// #[derive(Debug)] +/// pub struct Item +/// where +/// L: Link +/// { +/// link: L, +/// key: u64, +/// } +/// +/// intrusive_adapter! { ItemAdapter = Arc>: Item { link: L} where L: Link } +/// key_adapter! { ItemAdapter = Item { key: u64 } where L: Link } +/// ``` +#[macro_export] +macro_rules! intrusive_adapter { + (@impl + $vis:vis $name:ident ($($args:tt),*) = $pointer:ty: $item:path { $field:ident: $link:ty } $($where_:tt)* + ) => { + $vis struct $name<$($args),*> $($where_)* { + _marker: std::marker::PhantomData<($pointer, $($args),*)> + } + + unsafe impl<$($args),*> Send for $name<$($args),*> $($where_)* {} + unsafe impl<$($args),*> Sync for $name<$($args),*> $($where_)* {} + + unsafe impl<$($args),*> $crate::core::adapter::Adapter for $name<$($args),*> $($where_)*{ + type Pointer = $pointer; + type Link = $link; + + fn new() -> Self { + Self { + _marker: std::marker::PhantomData, + } + } + + unsafe fn link2item( + &self, + link: std::ptr::NonNull, + ) -> std::ptr::NonNull<::Item> { + std::ptr::NonNull::new_unchecked($crate::container_of!(link.as_ptr(), $item, $field) as *mut _) + } + + unsafe fn item2link( + &self, + item: std::ptr::NonNull<::Item>, + ) -> std::ptr::NonNull { + std::ptr::NonNull::new_unchecked((item.as_ptr() as *mut u8).add($crate::offset_of!($item, $field)) as *mut Self::Link) + } + } + + impl<$($args),*> std::fmt::Debug for $name<$($args),*> $($where_)*{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct(stringify!($name)).finish() + } + } + }; + ( + $vis:vis $name:ident = $($rest:tt)* + ) => { + intrusive_adapter! {@impl + $vis $name () = $($rest)* + } + }; + ( + $vis:vis $name:ident<$($args:tt),*> = $($rest:tt)* + ) => { + intrusive_adapter! {@impl + $vis $name ($($args),*) = $($rest)* + } + }; +} + +/// Macro to generate an implementation of [`KeyAdapter`] for instrusive container and items. +/// /// +/// The basic syntax to create an adapter is: +/// +/// ```rust,ignore +/// key_adapter! { Adapter = Item { key_field: KeyType } } +/// ``` +/// +/// # Generics +/// +/// This macro supports generic arguments: +/// +/// Note that due to macro parsing limitations, `T: Trait` bounds are not +/// supported in the generic argument list. You must list any trait bounds in +/// a separate `where` clause at the end of the macro. +/// +/// # Examples +/// +/// ``` +/// use foyer_intrusive::{intrusive_adapter, key_adapter}; +/// use foyer_intrusive::core::adapter::{Adapter, KeyAdapter, Link}; +/// use foyer_intrusive::core::pointer::Pointer; +/// use foyer_intrusive::eviction::EvictionPolicy; +/// use std::sync::Arc; +/// +/// #[derive(Debug)] +/// pub struct Item +/// where +/// L: Link +/// { +/// link: L, +/// key: u64, +/// } +/// +/// intrusive_adapter! { ItemAdapter = Arc>: Item { link: L} where L: Link } +/// key_adapter! { ItemAdapter = Item { key: u64 } where L: Link } +/// ``` +#[macro_export] +macro_rules! key_adapter { + (@impl + $adapter:ident ($($args:tt),*) = $item:ty { $field:ident: $key:ty } $($where_:tt)* + ) => { + unsafe impl<$($args),*> $crate::core::adapter::KeyAdapter for $adapter<$($args),*> $($where_)*{ + type Key = $key; + + unsafe fn item2key( + &self, + item: std::ptr::NonNull<::Item>, + ) -> std::ptr::NonNull { + std::ptr::NonNull::new_unchecked((item.as_ptr() as *const u8).add($crate::offset_of!($item, $field)) as *mut _) + } + } + }; + ( + $name:ident = $($rest:tt)* + ) => { + key_adapter! {@impl + $name () = $($rest)* + } + }; + ( + $name:ident<$($args:tt),*> = $($rest:tt)* + ) => { + key_adapter! {@impl + $name ($($args),*) = $($rest)* + } + }; +} + +/// Macro to generate an implementation of [`PriorityAdapter`] for instrusive container and items. +/// /// +/// The basic syntax to create an adapter is: +/// +/// ```rust,ignore +/// priority_adapter! { Adapter = Item { priority_field: PriorityType } } +/// ``` +/// +/// # Generics +/// +/// This macro supports generic arguments: +/// +/// Note that due to macro parsing limitations, `T: Trait` bounds are not +/// supported in the generic argument list. You must list any trait bounds in +/// a separate `where` clause at the end of the macro. +/// +/// # Examples +/// +/// ``` +/// use foyer_intrusive::{intrusive_adapter, priority_adapter}; +/// use foyer_intrusive::core::adapter::{Adapter, PriorityAdapter, Link}; +/// use foyer_intrusive::core::pointer::Pointer; +/// use foyer_intrusive::eviction::EvictionPolicy; +/// use std::sync::Arc; +/// +/// #[derive(Debug)] +/// pub struct Item +/// where +/// L: Link +/// { +/// link: L, +/// priority: usize, +/// } +/// +/// intrusive_adapter! { ItemAdapter = Arc>: Item { link: L} where L: Link } +/// priority_adapter! { ItemAdapter = Item { priority: usize } where L: Link } +/// ``` +#[macro_export] +macro_rules! priority_adapter { + (@impl + $adapter:ident ($($args:tt),*) = $item:ty { $field:ident: $priority:ty } $($where_:tt)* + ) => { + unsafe impl<$($args),*> $crate::core::adapter::PriorityAdapter for $adapter<$($args),*> $($where_)*{ + type Priority = $priority; + + unsafe fn item2priority( + &self, + item: std::ptr::NonNull< ::Item>, + ) -> std::ptr::NonNull{ + std::ptr::NonNull::new_unchecked((item.as_ptr() as *const u8).add($crate::offset_of!($item, $field)) as *mut _) + } + } + }; + ( + $name:ident = $($rest:tt)* + ) => { + priority_adapter! {@impl + $name () = $($rest)* + } + }; + ( + $name:ident<$($args:tt),*> = $($rest:tt)* + ) => { + priority_adapter! {@impl + $name ($($args),*) = $($rest)* + } + }; +} + +#[cfg(test)] +mod tests { + use itertools::Itertools; + + use super::*; + use crate::{collections::dlist::*, intrusive_adapter}; + + #[derive(Debug)] + struct DlistItem { + link: DlistLink, + val: u64, + } + + impl DlistItem { + fn new(val: u64) -> Self { + Self { + link: DlistLink::default(), + val, + } + } + } + + intrusive_adapter! { DlistItemAdapter = Box: DlistItem { link: DlistLink }} + key_adapter! { DlistItemAdapter = DlistItem { val: u64 } } + + #[test] + fn test_adapter_macro() { + let mut l = Dlist::::new(); + l.push_front(Box::new(DlistItem::new(1))); + let v = l.iter().map(|item| item.val).collect_vec(); + assert_eq!(v, vec![1]); + } +} diff --git a/src/utils/mod.rs b/foyer-intrusive/src/core/mod.rs similarity index 88% rename from src/utils/mod.rs rename to foyer-intrusive/src/core/mod.rs index 269a9d39..98f30086 100644 --- a/src/utils/mod.rs +++ b/foyer-intrusive/src/core/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 MrCroxx +// Copyright 2024 Foyer Project Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,5 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod count_min_sketch; -pub mod hash; +pub mod adapter; +pub mod pointer; diff --git a/foyer-intrusive/src/core/pointer.rs b/foyer-intrusive/src/core/pointer.rs new file mode 100644 index 00000000..7bac29f2 --- /dev/null +++ b/foyer-intrusive/src/core/pointer.rs @@ -0,0 +1,493 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2020 Amari Robinson +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, pin::Pin, ptr::NonNull, rc::Rc, sync::Arc}; + +/// # Safety +/// +/// Pointer operations MUST be valid. +pub unsafe trait Pointer { + type Item: ?Sized; + + /// # Safety + /// + /// Pointer operations MUST be valid. + unsafe fn from_ptr(item: *const Self::Item) -> Self; + + fn into_ptr(self) -> *const Self::Item; + + fn as_ptr(&self) -> *const Self::Item; +} + +/// # Safety +/// +/// Pointer operations MUST be valid. +pub unsafe trait DowngradablePointerOps: Pointer { + type WeakPointer; + + fn downgrade(&self) -> Self::WeakPointer; +} + +unsafe impl<'a, T: ?Sized + Debug> Pointer for &'a T { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> &'a T { + &*raw + } + + #[inline] + fn into_ptr(self) -> *const T { + self + } + + #[inline] + fn as_ptr(&self) -> *const T { + *self as *const _ + } +} + +unsafe impl<'a, T: ?Sized + Debug> Pointer for Pin<&'a T> { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Pin<&'a T> { + Pin::new_unchecked(&*raw) + } + + #[inline] + fn into_ptr(self) -> *const T { + unsafe { Pin::into_inner_unchecked(self) as *const T } + } + + #[inline] + fn as_ptr(&self) -> *const T { + self.get_ref() as *const _ + } +} + +unsafe impl Pointer for NonNull { + type Item = T; + + unsafe fn from_ptr(raw: *const T) -> NonNull { + NonNull::new_unchecked(raw as *mut _) + } + + fn into_ptr(self) -> *const T { + self.as_ptr() + } + + #[inline] + fn as_ptr(&self) -> *const T { + NonNull::as_ptr(*self) + } +} + +unsafe impl Pointer for Box { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Box { + Box::from_raw(raw as *mut T) + } + + #[inline] + fn into_ptr(self) -> *const T { + Box::into_raw(self) as *const T + } + + #[inline] + fn as_ptr(&self) -> *const T { + self.as_ref() as *const _ + } +} + +unsafe impl Pointer for Pin> { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Pin> { + Pin::new_unchecked(Box::from_raw(raw as *mut T)) + } + + #[inline] + fn into_ptr(self) -> *const T { + Box::into_raw(unsafe { Pin::into_inner_unchecked(self) }) as *const T + } + + #[inline] + fn as_ptr(&self) -> *const T { + self.as_ref().get_ref() as *const _ + } +} + +unsafe impl Pointer for Rc { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Rc { + Rc::from_raw(raw) + } + + #[inline] + fn into_ptr(self) -> *const T { + Rc::into_raw(self) + } + + #[inline] + fn as_ptr(&self) -> *const T { + Rc::as_ptr(self) + } +} + +unsafe impl Pointer for Pin> { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Pin> { + Pin::new_unchecked(Rc::from_raw(raw)) + } + + #[inline] + fn into_ptr(self) -> *const T { + Rc::into_raw(unsafe { Pin::into_inner_unchecked(self) }) + } + + #[inline] + fn as_ptr(&self) -> *const T { + self.as_ref().get_ref() as *const _ + } +} + +unsafe impl Pointer for Arc { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Arc { + Arc::from_raw(raw) + } + + #[inline] + fn into_ptr(self) -> *const T { + Arc::into_raw(self) + } + + #[inline] + fn as_ptr(&self) -> *const T { + Arc::as_ptr(self) + } +} + +unsafe impl Pointer for Pin> { + type Item = T; + + #[inline] + unsafe fn from_ptr(raw: *const T) -> Pin> { + Pin::new_unchecked(Arc::from_raw(raw)) + } + + #[inline] + fn into_ptr(self) -> *const T { + Arc::into_raw(unsafe { Pin::into_inner_unchecked(self) }) + } + + #[inline] + fn as_ptr(&self) -> *const T { + self.as_ref().get_ref() as *const _ + } +} + +unsafe impl DowngradablePointerOps for Rc { + type WeakPointer = std::rc::Weak; + + fn downgrade(&self) -> Self::WeakPointer { + Rc::downgrade(self) + } +} + +unsafe impl DowngradablePointerOps for Arc { + type WeakPointer = std::sync::Weak; + + fn downgrade(&self) -> Self::WeakPointer { + Arc::downgrade(self) + } +} + +#[cfg(test)] +mod tests { + use std::{boxed::Box, fmt::Debug, mem, pin::Pin, rc::Rc, sync::Arc}; + + use super::*; + + /// Clones a `Pointer` from a `*const Pointer::Value` + /// + /// This method is only safe to call if the raw pointer is known to be + /// managed by the provided `Pointer` type. + #[inline] + unsafe fn clone_pointer_from_raw(ptr: *const T::Item) -> T { + use std::{mem::ManuallyDrop, ops::Deref}; + + /// Guard which converts an pointer back into its raw version + /// when it gets dropped. This makes sure we also perform a full + /// `from_raw` and `into_raw` round trip - even in the case of panics. + struct PointerGuard { + pointer: ManuallyDrop, + } + + impl Drop for PointerGuard { + #[inline] + fn drop(&mut self) { + // Prevent shared pointers from being released by converting them + // back into the raw pointers + // SAFETY: `pointer` is never dropped. `ManuallyDrop::take` is not stable until 1.42.0. + let _ = T::into_ptr(unsafe { core::ptr::read(&*self.pointer) }); + } + } + + let holder = PointerGuard { + pointer: ManuallyDrop::new(T::from_ptr(ptr)), + }; + holder.pointer.deref().clone() + } + + #[test] + fn test_box() { + unsafe { + let p = Box::new(1); + let a: *const i32 = &*p; + let r = p.into_ptr(); + assert_eq!(a, r); + let p2: Box = as Pointer>::from_ptr(r); + let a2: *const i32 = &*p2; + assert_eq!(a, a2); + } + } + + #[test] + fn test_rc() { + unsafe { + let p = Rc::new(1); + let a: *const i32 = &*p; + let r = p.into_ptr(); + assert_eq!(a, r); + let p2: Rc = as Pointer>::from_ptr(r); + let a2: *const i32 = &*p2; + assert_eq!(a, a2); + } + } + + #[test] + fn test_arc() { + unsafe { + let p = Arc::new(1); + let a: *const i32 = &*p; + let r = p.into_ptr(); + assert_eq!(a, r); + let p2: Arc = as Pointer>::from_ptr(r); + let a2: *const i32 = &*p2; + assert_eq!(a, a2); + } + } + + #[test] + fn test_box_unsized() { + unsafe { + let p = Box::new(1) as Box; + let a: *const dyn Debug = &*p; + let b: (usize, usize) = mem::transmute(a); + let r = p.into_ptr(); + assert_eq!(a, r); + assert_eq!(b, mem::transmute(r)); + let p2: Box = as Pointer>::from_ptr(r); + let a2: *const dyn Debug = &*p2; + assert_eq!(a, a2); + assert_eq!(b, mem::transmute(a2)); + } + } + + #[test] + fn test_rc_unsized() { + unsafe { + let p = Rc::new(1) as Rc; + let a: *const dyn Debug = &*p; + let b: (usize, usize) = mem::transmute(a); + let r = p.into_ptr(); + assert_eq!(a, r); + assert_eq!(b, mem::transmute(r)); + let p2: Rc = as Pointer>::from_ptr(r); + let a2: *const dyn Debug = &*p2; + assert_eq!(a, a2); + assert_eq!(b, mem::transmute(a2)); + } + } + + #[test] + fn test_arc_unsized() { + unsafe { + let p = Arc::new(1) as Arc; + let a: *const dyn Debug = &*p; + let b: (usize, usize) = mem::transmute(a); + let r = p.into_ptr(); + assert_eq!(a, r); + assert_eq!(b, mem::transmute(r)); + let p2: Arc = as Pointer>::from_ptr(r); + let a2: *const dyn Debug = &*p2; + assert_eq!(a, a2); + assert_eq!(b, mem::transmute(a2)); + } + } + + #[test] + fn clone_arc_from_raw() { + unsafe { + let p = Arc::new(1); + let raw = Arc::as_ptr(&p); + let p2: Arc = clone_pointer_from_raw(raw); + assert_eq!(2, Arc::strong_count(&p2)); + } + } + + #[test] + fn clone_rc_from_raw() { + unsafe { + let p = Rc::new(1); + let raw = Rc::as_ptr(&p); + let p2: Rc = clone_pointer_from_raw(raw); + assert_eq!(2, Rc::strong_count(&p2)); + } + } + + #[test] + fn test_pin_box() { + unsafe { + let p = Pin::new(Box::new(1)); + let a: *const i32 = &*p; + let r = p.into_ptr(); + assert_eq!(a, r); + let p2: Pin> = > as Pointer>::from_ptr(r); + let a2: *const i32 = &*p2; + assert_eq!(a, a2); + } + } + + #[test] + fn test_pin_rc() { + unsafe { + let p = Pin::new(Rc::new(1)); + let a: *const i32 = &*p; + let r = p.into_ptr(); + assert_eq!(a, r); + let p2: Pin> = > as Pointer>::from_ptr(r); + let a2: *const i32 = &*p2; + assert_eq!(a, a2); + } + } + + #[test] + fn test_pin_arc() { + unsafe { + let p = Pin::new(Arc::new(1)); + let a: *const i32 = &*p; + let r = p.into_ptr(); + assert_eq!(a, r); + let p2: Pin> = > as Pointer>::from_ptr(r); + let a2: *const i32 = &*p2; + assert_eq!(a, a2); + } + } + + #[test] + fn test_pin_box_unsized() { + unsafe { + let p = Pin::new(Box::new(1)) as Pin>; + let a: *const dyn Debug = &*p; + let b: (usize, usize) = mem::transmute(a); + let r = p.into_ptr(); + assert_eq!(a, r); + assert_eq!(b, mem::transmute(r)); + let p2: Pin> = > as Pointer>::from_ptr(r); + let a2: *const dyn Debug = &*p2; + assert_eq!(a, a2); + assert_eq!(b, mem::transmute(a2)); + } + } + + #[test] + fn test_pin_rc_unsized() { + unsafe { + let p = Pin::new(Rc::new(1)) as Pin>; + let a: *const dyn Debug = &*p; + let b: (usize, usize) = mem::transmute(a); + let r = p.into_ptr(); + assert_eq!(a, r); + assert_eq!(b, mem::transmute(r)); + let p2: Pin> = > as Pointer>::from_ptr(r); + let a2: *const dyn Debug = &*p2; + assert_eq!(a, a2); + assert_eq!(b, mem::transmute(a2)); + } + } + + #[test] + fn test_pin_arc_unsized() { + unsafe { + let p = Pin::new(Arc::new(1)) as Pin>; + let a: *const dyn Debug = &*p; + let b: (usize, usize) = mem::transmute(a); + let r = p.into_ptr(); + assert_eq!(a, r); + assert_eq!(b, mem::transmute(r)); + let p2: Pin> = > as Pointer>::from_ptr(r); + let a2: *const dyn Debug = &*p2; + assert_eq!(a, a2); + assert_eq!(b, mem::transmute(a2)); + } + } + + #[test] + fn clone_pin_arc_from_raw() { + unsafe { + let p = Pin::new(Arc::new(1)); + let raw = p.into_ptr(); + let p2: Pin> = clone_pointer_from_raw(raw); + let _p = > as Pointer>::from_ptr(raw); + assert_eq!(2, Arc::strong_count(&Pin::into_inner(p2))); + } + } + + #[test] + fn clone_pin_rc_from_raw() { + unsafe { + let p = Pin::new(Rc::new(1)); + let raw = p.into_ptr(); + let p2: Pin> = clone_pointer_from_raw(raw); + let _p = > as Pointer>::from_ptr(raw); + assert_eq!(2, Rc::strong_count(&Pin::into_inner(p2))); + } + } +} diff --git a/foyer-intrusive/src/eviction/fifo.rs b/foyer-intrusive/src/eviction/fifo.rs new file mode 100644 index 00000000..50810894 --- /dev/null +++ b/foyer-intrusive/src/eviction/fifo.rs @@ -0,0 +1,336 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{mem::ManuallyDrop, ptr::NonNull}; + +use super::EvictionPolicy; +use crate::{ + collections::dlist::{Dlist, DlistIter, DlistLink}, + core::{ + adapter::{Adapter, Link}, + pointer::Pointer, + }, + intrusive_adapter, +}; + +#[derive(Debug, Clone)] +pub struct FifoConfig; + +#[derive(Debug, Default)] +pub struct FifoLink { + link: DlistLink, +} + +impl FifoLink { + fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } +} + +impl Link for FifoLink { + fn is_linked(&self) -> bool { + self.link.is_linked() + } +} + +intrusive_adapter! { FifoLinkAdapter = NonNull: FifoLink { link: DlistLink } } + +/// FIFO policy +#[derive(Debug)] +pub struct Fifo +where + A: Adapter, + ::Pointer: Clone, +{ + queue: Dlist, + + _config: FifoConfig, + + len: usize, + + adapter: A, +} + +impl Drop for Fifo +where + A: Adapter, + ::Pointer: Clone, +{ + fn drop(&mut self) { + let mut to_remove = vec![]; + for ptr in self.iter() { + to_remove.push(ptr.clone()); + } + for ptr in to_remove { + self.remove(&ptr); + } + } +} + +impl Fifo +where + A: Adapter, + ::Pointer: Clone, +{ + pub fn new(config: FifoConfig) -> Self { + Self { + queue: Dlist::new(), + + _config: config, + + len: 0, + + adapter: A::new(), + } + } + + fn insert(&mut self, ptr: A::Pointer) { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(!link.as_ref().is_linked()); + + self.queue.push_back(link); + + self.len += 1; + } + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::as_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(link.as_ref().is_linked()); + + self.queue.iter_mut_from_raw(link.as_ref().link.raw()).remove().unwrap(); + + self.len -= 1; + + A::Pointer::from_ptr(item.as_ptr()) + } + } + + fn access(&mut self, _ptr: &A::Pointer) {} + + fn len(&self) -> usize { + self.len + } + + fn iter(&self) -> FifoIter { + let mut iter = self.queue.iter(); + iter.front(); + + FifoIter { + fifo: self, + iter, + + ptr: ManuallyDrop::new(None), + } + } +} + +pub struct FifoIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ + fifo: &'a Fifo, + iter: DlistIter<'a, FifoLinkAdapter>, + + ptr: ManuallyDrop::Pointer>>, +} + +impl<'a, A> FifoIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ + unsafe fn update_ptr(&mut self, link: NonNull) { + std::mem::forget(self.ptr.take()); + + let item = self.fifo.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + self.ptr = ManuallyDrop::new(Some(ptr)); + } + + unsafe fn ptr(&self) -> Option<&'a ::Pointer> { + if self.ptr.is_none() { + return None; + } + let ptr = self.ptr.as_ref().unwrap(); + let raw = ptr as *const ::Pointer; + Some(&*raw) + } +} + +impl<'a, A> Iterator for FifoIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ + type Item = &'a A::Pointer; + + fn next(&mut self) -> Option { + unsafe { + let link = match self.iter.get() { + Some(link) => { + let link = link.raw(); + self.iter.next(); + link + } + None => return None, + }; + self.update_ptr(link); + self.ptr() + } + } +} + +// unsafe impl `Send + Sync` for structs with `NonNull` usage + +unsafe impl Send for Fifo +where + A: Adapter, + ::Pointer: Clone, +{ +} + +unsafe impl Sync for Fifo +where + A: Adapter, + ::Pointer: Clone, +{ +} + +unsafe impl Send for FifoLink {} + +unsafe impl Sync for FifoLink {} + +unsafe impl<'a, A> Send for FifoIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ +} + +unsafe impl<'a, A> Sync for FifoIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ +} + +impl EvictionPolicy for Fifo +where + A: Adapter, + ::Pointer: Clone, +{ + type Adapter = A; + type Config = FifoConfig; + + fn new(config: Self::Config) -> Self { + Self::new(config) + } + + fn insert(&mut self, ptr: A::Pointer) { + self.insert(ptr) + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + self.remove(ptr) + } + + fn access(&mut self, ptr: &A::Pointer) { + self.access(ptr) + } + + fn len(&self) -> usize { + self.len() + } + + fn iter(&self) -> impl Iterator { + self.iter() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::eviction::EvictionPolicyExt; + + #[derive(Debug)] + struct FifoItem { + link: FifoLink, + key: u64, + } + + impl FifoItem { + fn new(key: u64) -> Self { + Self { + link: FifoLink::default(), + key, + } + } + } + + intrusive_adapter! { FifoItemAdapter = Arc: FifoItem { link: FifoLink } } + + #[test] + fn test_fifo_simple() { + let config = FifoConfig; + let mut fifo = Fifo::::new(config); + + let mut items = vec![]; + + for key in 0..10 { + let item = Arc::new(FifoItem::new(key)); + fifo.push(item.clone()); + items.push(item); + } + let v = fifo.iter().map(|item| item.key).collect_vec(); + assert_eq!(v, (0..10).collect_vec()); + + let v = (0..5).map(|_| fifo.pop().unwrap()).map(|item| item.key).collect_vec(); + assert_eq!(v, (0..5).collect_vec()); + + let v = fifo.iter().map(|item| item.key).collect_vec(); + assert_eq!(v, (5..10).collect_vec()); + + drop(fifo); + + for item in items { + assert_eq!(Arc::strong_count(&item), 1); + } + } +} diff --git a/foyer-intrusive/src/eviction/lfu.rs b/foyer-intrusive/src/eviction/lfu.rs new file mode 100644 index 00000000..dc62da52 --- /dev/null +++ b/foyer-intrusive/src/eviction/lfu.rs @@ -0,0 +1,626 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + hash::{Hash, Hasher}, + mem::ManuallyDrop, + ptr::NonNull, +}; + +use cmsketch::CMSketchUsize; +use twox_hash::XxHash64; + +use super::EvictionPolicy; +use crate::{ + collections::dlist::{Dlist, DlistIter, DlistLink}, + core::{ + adapter::{Adapter, KeyAdapter, Link}, + pointer::Pointer, + }, + intrusive_adapter, +}; + +const MIN_CAPACITY: usize = 100; +const ERROR_THRESHOLD: f64 = 5.0; +const HASH_COUNT: usize = 4; +const DECAY_FACTOR: f64 = 0.5; + +#[derive(Debug, Clone)] +pub struct LfuConfig { + /// The multiplier for window len given the cache size. + pub window_to_cache_size_ratio: usize, + + /// The ratio of tiny lru capacity to overall capacity. + pub tiny_lru_capacity_ratio: f64, +} + +#[derive(PartialEq, Eq, Debug)] +enum LruType { + Tiny, + Main, + + None, +} + +#[derive(Debug, Default)] +pub struct LfuLink { + link_tiny: DlistLink, + link_main: DlistLink, +} + +impl Link for LfuLink { + fn is_linked(&self) -> bool { + self.link_tiny.is_linked() || self.link_main.is_linked() + } +} + +impl LfuLink { + fn lru_type(&self) -> LruType { + match (self.link_tiny.is_linked(), self.link_main.is_linked()) { + (true, true) => unreachable!(), + (true, false) => LruType::Tiny, + (false, true) => LruType::Main, + (false, false) => LruType::None, + } + } + + fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } +} + +intrusive_adapter! { LfuLinkTinyDlistAdapter = NonNull: LfuLink { link_tiny: DlistLink } } +intrusive_adapter! { LfuLinkMainDlistAdapter = NonNull: LfuLink { link_main: DlistLink } } + +/// Implements the W-TinyLFU cache eviction policy as described in - +/// +/// https://arxiv.org/pdf/1512.00727.pdf +/// +/// The cache is split into 2 parts, the main cache and the tiny cache. +/// The tiny cache is typically sized to be 1% of the total cache with +/// the main cache being the rest 99%. Both caches are implemented using +/// LRUs. New items land in tiny cache. During eviction, the tail item +/// from the tiny cache is promoted to main cache if its frequency is +/// higher than the tail item of of main cache, and the tail of main +/// cache is evicted. This gives the frequency based admission into main +/// cache. Hits in each cache simply move the item to the head of each +/// LRU cache. +/// The frequency counts are maintained in count-min-sketch approximate +/// counters - +/// +/// Counter Overhead: +/// The window_to_cache_size_ratio determines the size of counters. +/// The default value is 32 which means the counting window size is +/// 32 times the cache size. After every 32 X cache capacity number +/// of items, the counts are halved to weigh frequency by recency. +/// The function counter_size() returns the size of the counters +/// in bytes. See maybe_grow_access_counters() implementation for +/// how the size is computed. +/// +/// Tiny cache size: +/// This default to 1%. There's no need to tune this parameter. +#[derive(Debug)] +pub struct Lfu +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + /// tiny lru list + lru_tiny: Dlist, + + /// main lru list + lru_main: Dlist, + + /// the window length counter + window_size: usize, + + /// maximum value of window length which when hit the counters are halved + max_window_size: usize, + + /// the capacity for which the counters are sized + capacity: usize, + + /// approximate streaming frequency counters + /// + /// the counts are halved every time the max_window_len is hit + frequencies: CMSketchUsize, + + len: usize, + + config: LfuConfig, + + adapter: A, +} + +impl Drop for Lfu +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + fn drop(&mut self) { + let mut to_remove = vec![]; + for ptr in self.iter() { + to_remove.push(ptr.clone()); + } + for ptr in to_remove { + self.remove(&ptr); + } + } +} + +impl Lfu +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + pub fn new(config: LfuConfig) -> Self { + let mut res = Self { + lru_tiny: Dlist::new(), + lru_main: Dlist::new(), + + window_size: 0, + max_window_size: 0, + capacity: 0, + + // A dummy size, will be updated later. + frequencies: CMSketchUsize::new_with_size(1, 1), + + len: 0, + + config, + + adapter: A::new(), + }; + res.maybe_grow_access_counters(); + res + } + + fn insert(&mut self, ptr: A::Pointer) { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(!link.as_ref().is_linked()); + + self.lru_tiny.push_front(link); + + // Initialize the frequency count for this link. + self.update_frequencies(link); + + // If tiny cache is full, unconditionally promote tail to main cache. + let expected_tiny_len = + (self.config.tiny_lru_capacity_ratio * (self.lru_tiny.len() + self.lru_main.len()) as f64) as usize; + if self.lru_tiny.len() > expected_tiny_len { + let raw = self.lru_tiny.back().unwrap().raw(); + self.switch_to_lru_front(raw); + } else { + self.maybe_promote_tail(); + } + + // If the number of counters are too small for the cache size, double them. + self.maybe_grow_access_counters(); + + self.len += 1; + } + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::as_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(link.as_ref().is_linked()); + + self.remove_from_lru(link); + + self.len -= 1; + + A::Pointer::from_ptr(item.as_ptr()) + } + } + + fn access(&mut self, ptr: &A::Pointer) { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::as_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(link.as_ref().is_linked()); + + self.move_to_lru_front(link); + + self.update_frequencies(link); + } + } + + fn len(&self) -> usize { + self.len + } + + fn iter(&self) -> LfuIter { + let mut iter_main = self.lru_main.iter(); + let mut iter_tiny = self.lru_tiny.iter(); + + iter_main.back(); + iter_tiny.back(); + + LfuIter { + lfu: self, + iter_main, + iter_tiny, + + ptr: ManuallyDrop::new(None), + } + } + + fn maybe_grow_access_counters(&mut self) { + let capacity = self.lru_tiny.len() + self.lru_main.len(); + + // If the new capacity ask is more than double the current size, + // recreate the approximate frequency counters. + if 2 * self.capacity > capacity { + return; + } + + self.capacity = std::cmp::max(capacity, MIN_CAPACITY); + + // The window counter that's incremented on every fetch. + self.window_size = 0; + + // The frequency counters are halved every `max_window_size` fetches to decay the frequency counts. + self.max_window_size = self.capacity * self.config.window_to_cache_size_ratio; + + // Number of frequency counters - roughly equal to the window size divided by error tolerance. + let num_counters = (1f64.exp() * self.max_window_size as f64 / ERROR_THRESHOLD) as usize; + let num_counters = num_counters.next_power_of_two(); + + self.frequencies = CMSketchUsize::new_with_size(num_counters, HASH_COUNT); + } + + unsafe fn update_frequencies(&mut self, link: NonNull) { + self.frequencies.record(self.hash_link(link)); + self.window_size += 1; + + // Decay counts every `max_window_size`. This avoids having items that were + // accessed frequently (were hot) but aren't being accessed anymore (are cold) + // from staying in cache forever. + if self.window_size == self.max_window_size { + self.window_size >>= 1; + self.frequencies.decay(DECAY_FACTOR); + } + } + + fn maybe_promote_tail(&mut self) { + unsafe { + let link_main = match self.lru_main.back() { + Some(link) => link.raw(), + None => return, + }; + let link_tiny = match self.lru_tiny.back() { + Some(link) => link.raw(), + None => return, + }; + + if self.admit_to_main(link_main, link_tiny) { + self.switch_to_lru_front(link_main); + self.switch_to_lru_front(link_tiny); + return; + } + + // A node with high frequency at the tail of main cache might prevent + // promotions from tiny cache from happening for a long time. Relocate + // the tail of main cache to prevent this. + self.move_to_lru_front(link_main); + } + } + + fn admit_to_main(&self, link_main: NonNull, link_tiny: NonNull) -> bool { + unsafe { + assert_eq!(link_main.as_ref().lru_type(), LruType::Main); + assert_eq!(link_tiny.as_ref().lru_type(), LruType::Tiny); + + let frequent_main = self.frequencies.count(self.hash_link(link_main)); + let frequent_tiny = self.frequencies.count(self.hash_link(link_tiny)); + + frequent_main <= frequent_tiny + } + } + + unsafe fn move_to_lru_front(&mut self, link: NonNull) { + match link.as_ref().lru_type() { + LruType::Tiny => { + let raw = link.as_ref().link_tiny.raw(); + let ptr = self.lru_tiny.iter_mut_from_raw(raw).remove().unwrap(); + self.lru_tiny.push_front(ptr); + } + LruType::Main => { + let raw = link.as_ref().link_main.raw(); + let ptr = self.lru_main.iter_mut_from_raw(raw).remove().unwrap(); + self.lru_main.push_front(ptr); + } + LruType::None => unreachable!(), + } + } + + unsafe fn switch_to_lru_front(&mut self, link: NonNull) { + match link.as_ref().lru_type() { + LruType::Tiny => { + let raw = link.as_ref().link_tiny.raw(); + let ptr = self.lru_tiny.iter_mut_from_raw(raw).remove().unwrap(); + self.lru_main.push_front(ptr); + } + LruType::Main => { + let raw = link.as_ref().link_main.raw(); + let ptr = self.lru_main.iter_mut_from_raw(raw).remove().unwrap(); + self.lru_tiny.push_front(ptr); + } + LruType::None => unreachable!(), + } + } + + unsafe fn remove_from_lru(&mut self, link: NonNull) { + match link.as_ref().lru_type() { + LruType::Tiny => { + let raw = link.as_ref().link_tiny.raw(); + self.lru_tiny.iter_mut_from_raw(raw).remove().unwrap(); + } + LruType::Main => { + let raw = link.as_ref().link_main.raw(); + self.lru_main.iter_mut_from_raw(raw).remove().unwrap(); + } + LruType::None => unreachable!(), + } + } + + fn hash_link(&self, link: NonNull) -> u64 { + let mut hasher = XxHash64::default(); + let key = unsafe { + let item = self.adapter.link2item(link); + let key = self.adapter.item2key(item); + key.as_ref() + }; + key.hash(&mut hasher); + hasher.finish() + } +} + +pub struct LfuIter<'a, A> +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + lfu: &'a Lfu, + iter_tiny: DlistIter<'a, LfuLinkTinyDlistAdapter>, + iter_main: DlistIter<'a, LfuLinkMainDlistAdapter>, + + ptr: ManuallyDrop::Pointer>>, +} + +impl<'a, A> LfuIter<'a, A> +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + unsafe fn update_ptr(&mut self, link: NonNull) { + std::mem::forget(self.ptr.take()); + + let item = self.lfu.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + self.ptr = ManuallyDrop::new(Some(ptr)); + } + + unsafe fn ptr(&self) -> Option<&'a ::Pointer> { + if self.ptr.is_none() { + return None; + } + let ptr = self.ptr.as_ref().unwrap(); + let raw = ptr as *const ::Pointer; + Some(&*raw) + } +} + +impl<'a, A> Iterator for LfuIter<'a, A> +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + type Item = &'a A::Pointer; + + fn next(&mut self) -> Option { + unsafe { + let link_main = self.iter_main.get(); + let link_tiny = self.iter_tiny.get(); + + let link = match (link_main, link_tiny) { + (None, None) => return None, + (Some(link_main), None) => { + let link = link_main.raw(); + self.iter_main.prev(); + link + } + (None, Some(link_tiny)) => { + let link = link_tiny.raw(); + self.iter_tiny.prev(); + link + } + (Some(link_main), Some(link_tiny)) => { + // Eviction from tiny or main depending on whether the tiny handle would be + // admitted to main cachce. If it would be, evict from main cache, otherwise + // from tiny cache. + if self.lfu.admit_to_main(link_main.raw(), link_tiny.raw()) { + let link = link_main.raw(); + self.iter_main.prev(); + link + } else { + let link = link_tiny.raw(); + self.iter_tiny.prev(); + link + } + } + }; + self.update_ptr(link); + self.ptr() + } + } +} + +// unsafe impl `Send + Sync` for structs with `NonNull` usage + +unsafe impl Send for Lfu +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ +} +unsafe impl Sync for Lfu +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ +} + +unsafe impl Send for LfuLink {} +unsafe impl Sync for LfuLink {} + +unsafe impl<'a, A> Send for LfuIter<'a, A> +where + A: Adapter + KeyAdapter, + + ::Pointer: Clone, +{ +} +unsafe impl<'a, A> Sync for LfuIter<'a, A> +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ +} + +impl EvictionPolicy for Lfu +where + A: Adapter + KeyAdapter, + ::Pointer: Clone, +{ + type Adapter = A; + type Config = LfuConfig; + + fn new(config: Self::Config) -> Self { + Self::new(config) + } + + fn insert(&mut self, ptr: A::Pointer) { + self.insert(ptr) + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + self.remove(ptr) + } + + fn access(&mut self, ptr: &A::Pointer) { + self.access(ptr) + } + + fn len(&self) -> usize { + self.len() + } + + fn iter(&self) -> impl Iterator { + self.iter() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::key_adapter; + + #[derive(Debug)] + struct LfuItem { + link: LfuLink, + key: u64, + } + + impl LfuItem { + fn new(key: u64) -> Self { + Self { + link: LfuLink::default(), + key, + } + } + } + + intrusive_adapter! { LfuItemAdapter = Arc: LfuItem { link: LfuLink } } + key_adapter! { LfuItemAdapter = LfuItem { key: u64 } } + + #[test] + fn test_lfu_simple() { + let config = LfuConfig { + window_to_cache_size_ratio: 10, + tiny_lru_capacity_ratio: 0.01, + }; + let mut lfu = Lfu::::new(config); + + let items = (0..101).map(LfuItem::new).map(Arc::new).collect_vec(); + for item in items.iter().take(100) { + lfu.insert(item.clone()); + } + + assert_eq!(99, lfu.lru_main.len()); + assert_eq!(1, lfu.lru_tiny.len()); + assert_eq!(items[0].link.lru_type(), LruType::Tiny); + + // 0 will be evicted at last because it is on tiny lru but its frequency equals to others + assert_eq!( + (1..100).chain([0].into_iter()).collect_vec(), + lfu.iter().map(|item| item.key).collect_vec() + ); + + for item in items.iter().take(100) { + lfu.access(item); + } + lfu.access(&items[0]); + lfu.insert(items[100].clone()); + + assert_eq!(items[0].link.lru_type(), LruType::Main); + assert_eq!(items[100].link.lru_type(), LruType::Tiny); + + assert_eq!( + [100].into_iter().chain(1..100).chain([0].into_iter()).collect_vec(), + lfu.iter().map(|item| item.key).collect_vec() + ); + + drop(lfu); + + for item in items { + assert_eq!(Arc::strong_count(&item), 1); + } + } +} diff --git a/foyer-intrusive/src/eviction/lru.rs b/foyer-intrusive/src/eviction/lru.rs new file mode 100644 index 00000000..752cb078 --- /dev/null +++ b/foyer-intrusive/src/eviction/lru.rs @@ -0,0 +1,466 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{mem::ManuallyDrop, ptr::NonNull}; + +use super::EvictionPolicy; +use crate::{ + collections::dlist::{Dlist, DlistIter, DlistLink}, + core::{ + adapter::{Adapter, Link}, + pointer::Pointer, + }, + intrusive_adapter, +}; + +#[derive(Clone, Debug)] +pub struct LruConfig { + /// Insertion point of the new entry, between 0 and 1. + pub lru_insertion_point_fraction: f64, +} + +#[derive(Debug, Default)] +pub struct LruLink { + link_lru: DlistLink, + + is_in_tail: bool, +} + +impl LruLink { + fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } +} + +impl Link for LruLink { + fn is_linked(&self) -> bool { + self.link_lru.is_linked() + } +} + +intrusive_adapter! { LruLinkAdapter = NonNull: LruLink { link_lru: DlistLink } } + +#[derive(Debug)] +pub struct Lru +where + A: Adapter, + ::Pointer: Clone, +{ + /// lru list + lru: Dlist, + + /// insertion point + insertion_point: Option>, + + /// length of tail after insertion point + tail_len: usize, + + len: usize, + + config: LruConfig, + + adapter: A, +} + +impl Drop for Lru +where + A: Adapter, + ::Pointer: Clone, +{ + fn drop(&mut self) { + let mut to_remove = vec![]; + for ptr in self.iter() { + to_remove.push(ptr.clone()); + } + for ptr in to_remove { + self.remove(&ptr); + } + } +} + +impl Lru +where + A: Adapter, + ::Pointer: Clone, +{ + fn new(config: LruConfig) -> Self { + Self { + lru: Dlist::new(), + + insertion_point: None, + + tail_len: 0, + + len: 0, + + config, + + adapter: A::new(), + } + } + + fn insert(&mut self, ptr: A::Pointer) { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(!link.as_ref().is_linked()); + + self.insert_lru(link); + + self.update_lru_insertion_point(); + + self.len += 1; + } + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::as_ptr(ptr) as *mut _); + let mut link = self.adapter.item2link(item); + + assert!(link.as_ref().is_linked()); + + self.ensure_not_insertion_point(link); + self.lru + .iter_mut_from_raw(link.as_ref().link_lru.raw()) + .remove() + .unwrap(); + if link.as_ref().is_in_tail { + link.as_mut().is_in_tail = false; + self.tail_len -= 1; + } + + self.len -= 1; + + A::Pointer::from_ptr(item.as_ptr()) + } + } + + fn access(&mut self, ptr: &A::Pointer) { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::as_ptr(ptr) as *mut _); + let mut link = self.adapter.item2link(item); + + assert!(link.as_ref().is_linked()); + + self.ensure_not_insertion_point(link); + + self.move_to_lru_front(link); + + if link.as_ref().is_in_tail { + link.as_mut().is_in_tail = false; + self.tail_len -= 1; + self.update_lru_insertion_point(); + } + } + } + + fn len(&self) -> usize { + self.len + } + + fn iter(&self) -> LruIter<'_, A> { + let mut iter = self.lru.iter(); + iter.back(); + LruIter { + iter, + lru: self, + ptr: ManuallyDrop::new(None), + } + } + + fn update_lru_insertion_point(&mut self) { + unsafe { + if self.config.lru_insertion_point_fraction == 0.0 { + return; + } + + if self.insertion_point.is_none() { + self.insertion_point = self.lru.back().map(LruLink::raw); + self.tail_len = 0; + if let Some(insertion_point) = &mut self.insertion_point { + insertion_point.as_mut().is_in_tail = true; + self.tail_len += 1; + } + } + + if self.lru.len() <= 1 { + return; + } + + assert!(self.insertion_point.is_some()); + + let expected_tail_len = (self.lru.len() as f64 * (1.0 - self.config.lru_insertion_point_fraction)) as usize; + + let mut curr = self.insertion_point.unwrap(); + while self.tail_len < expected_tail_len && Some(curr) != self.lru.front().map(LruLink::raw) { + curr = self.lru_prev(curr).unwrap(); + curr.as_mut().is_in_tail = true; + self.tail_len += 1; + } + while self.tail_len > expected_tail_len && Some(curr) != self.lru.back().map(LruLink::raw) { + curr.as_mut().is_in_tail = false; + self.tail_len -= 1; + curr = self.lru_next(curr).unwrap(); + } + + self.insertion_point = Some(curr); + } + } + + unsafe fn ensure_not_insertion_point(&mut self, link: NonNull) { + if Some(link) == self.insertion_point { + self.insertion_point = self.lru_prev(link); + match &mut self.insertion_point { + Some(insertion_point) => { + self.tail_len += 1; + insertion_point.as_mut().is_in_tail = true; + } + // TODO(MrCroxx): think ? + None => assert_eq!(self.lru.len(), 1), + } + } + } + + unsafe fn insert_lru(&mut self, link: NonNull) { + match self.insertion_point { + Some(insertion_point) => self + .lru + .iter_mut_from_raw(insertion_point.as_ref().link_lru.raw()) + .insert_before(link), + None => self.lru.push_front(link), + } + } + + unsafe fn move_to_lru_front(&mut self, link: NonNull) { + self.lru + .iter_mut_from_raw(link.as_ref().link_lru.raw()) + .remove() + .unwrap(); + self.lru.push_front(link); + } + + unsafe fn lru_prev(&self, link: NonNull) -> Option> { + let mut iter = self.lru.iter_from_raw(link.as_ref().link_lru.raw()); + iter.prev(); + iter.get().map(LruLink::raw) + } + + unsafe fn lru_next(&self, link: NonNull) -> Option> { + let mut iter = self.lru.iter_from_raw(link.as_ref().link_lru.raw()); + iter.next(); + iter.get().map(LruLink::raw) + } +} + +pub struct LruIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ + lru: &'a Lru, + iter: DlistIter<'a, LruLinkAdapter>, + + ptr: ManuallyDrop::Pointer>>, +} + +impl<'a, A> LruIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ + unsafe fn update_ptr(&mut self, link: NonNull) { + std::mem::forget(self.ptr.take()); + + let item = self.lru.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + self.ptr = ManuallyDrop::new(Some(ptr)); + } + + unsafe fn ptr(&self) -> Option<&'a ::Pointer> { + if self.ptr.is_none() { + return None; + } + let ptr = self.ptr.as_ref().unwrap(); + let raw = ptr as *const ::Pointer; + Some(&*raw) + } +} + +impl<'a, A> Iterator for LruIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ + type Item = &'a A::Pointer; + + fn next(&mut self) -> Option { + unsafe { + let link = match self.iter.get() { + Some(link) => { + let link = link.raw(); + self.iter.prev(); + link + } + None => return None, + }; + self.update_ptr(link); + self.ptr() + } + } +} + +// unsafe impl `Send + Sync` for structs with `NonNull` usage + +unsafe impl Send for Lru +where + A: Adapter, + ::Pointer: Clone, +{ +} + +unsafe impl Sync for Lru +where + A: Adapter, + ::Pointer: Clone, +{ +} + +unsafe impl Send for LruLink {} + +unsafe impl Sync for LruLink {} + +unsafe impl<'a, A> Send for LruIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ +} + +unsafe impl<'a, A> Sync for LruIter<'a, A> +where + A: Adapter, + ::Pointer: Clone, +{ +} + +impl EvictionPolicy for Lru +where + A: Adapter, + ::Pointer: Clone, +{ + type Adapter = A; + type Config = LruConfig; + + fn new(config: Self::Config) -> Self { + Self::new(config) + } + + fn insert(&mut self, ptr: A::Pointer) { + self.insert(ptr) + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + self.remove(ptr) + } + + fn access(&mut self, ptr: &A::Pointer) { + self.access(ptr) + } + + fn len(&self) -> usize { + self.len() + } + + fn iter(&self) -> impl Iterator { + self.iter() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::key_adapter; + + #[derive(Debug)] + struct LruItem { + link: LruLink, + key: u64, + } + + impl LruItem { + fn new(key: u64) -> Self { + Self { + link: LruLink::default(), + key, + } + } + } + + intrusive_adapter! { LruItemAdapter = Arc: LruItem { link: LruLink } } + key_adapter! { LruItemAdapter = LruItem { key: u64 } } + + #[test] + fn test_lru_simple() { + let config = LruConfig { + lru_insertion_point_fraction: 0.0, + }; + let mut lru = Lru::::new(config); + + let handles = vec![ + Arc::new(LruItem::new(0)), + Arc::new(LruItem::new(1)), + Arc::new(LruItem::new(2)), + ]; + + lru.insert(handles[0].clone()); + lru.insert(handles[1].clone()); + lru.insert(handles[2].clone()); + + assert_eq!(vec![0, 1, 2], lru.iter().map(|item| item.key).collect_vec()); + + lru.access(&handles[1]); + + assert_eq!(vec![0, 2, 1], lru.iter().map(|item| item.key).collect_vec()); + + lru.remove(&handles[2]); + + assert_eq!(vec![0, 1], lru.iter().map(|item| item.key).collect_vec()); + + drop(lru); + + for handle in handles { + assert_eq!(Arc::strong_count(&handle), 1); + } + } +} diff --git a/foyer-intrusive/src/eviction/mod.rs b/foyer-intrusive/src/eviction/mod.rs new file mode 100644 index 00000000..263e24a9 --- /dev/null +++ b/foyer-intrusive/src/eviction/mod.rs @@ -0,0 +1,73 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; + +use crate::core::adapter::Adapter; + +pub trait EvictionPolicy: Send + Sync + Debug + 'static { + type Adapter: Adapter; + type Config: Send + Sync + 'static + Debug + Clone; + + fn new(config: Self::Config) -> Self; + + fn insert(&mut self, ptr: ::Pointer); + + fn remove(&mut self, ptr: &::Pointer) -> ::Pointer; + + fn access(&mut self, ptr: &::Pointer); + + fn len(&self) -> usize; + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn iter(&self) -> impl Iterator::Pointer> + '_; +} + +pub trait EvictionPolicyExt: EvictionPolicy { + fn push(&mut self, ptr: ::Pointer); + + fn pop(&mut self) -> Option<::Pointer>; + + fn peek(&self) -> Option<&::Pointer>; +} + +impl EvictionPolicyExt for E +where + ::Pointer: Clone, +{ + fn push(&mut self, ptr: ::Pointer) { + self.insert(ptr) + } + + fn pop(&mut self) -> Option<::Pointer> { + let ptr = { + let mut iter = self.iter(); + let ptr = iter.next(); + ptr.cloned() + }; + ptr.map(|ptr| self.remove(&ptr)) + } + + fn peek(&self) -> Option<&::Pointer> { + self.iter().next() + } +} + +pub mod fifo; +pub mod lfu; +pub mod lru; +pub mod sfifo; diff --git a/foyer-intrusive/src/eviction/sfifo.rs b/foyer-intrusive/src/eviction/sfifo.rs new file mode 100644 index 00000000..483c882b --- /dev/null +++ b/foyer-intrusive/src/eviction/sfifo.rs @@ -0,0 +1,442 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{mem::ManuallyDrop, ptr::NonNull}; + +use itertools::Itertools; + +use super::EvictionPolicy; +use crate::{ + collections::dlist::{Dlist, DlistIter, DlistLink}, + core::{ + adapter::{Adapter, Link, PriorityAdapter}, + pointer::Pointer, + }, + intrusive_adapter, +}; + +#[derive(Debug, Clone)] +pub struct SegmentedFifoConfig { + /// `segment_ratios` is used to compute the ratio of each segment's size. + /// + /// The formula is as follows: + /// + /// `segment's size = total_segments * (segment's ratio / sum(ratio))` + pub segment_ratios: Vec, +} + +#[derive(Debug, Default)] +pub struct SegmentedFifoLink { + link_queue: DlistLink, + + priority: usize, +} + +impl SegmentedFifoLink { + fn raw(&self) -> NonNull { + unsafe { NonNull::new_unchecked(self as *const _ as *mut _) } + } +} + +impl Link for SegmentedFifoLink { + fn is_linked(&self) -> bool { + self.link_queue.is_linked() + } +} + +intrusive_adapter! { SegmentedFifoLinkAdapter = NonNull: SegmentedFifoLink { link_queue: DlistLink } } + +/// Segmented FIFO policy +/// +/// It divides the fifo queue into N segments. Each segment holds +/// number of items proportional to its segment ratio. For example, +/// if we have 3 segments and the ratio of [2, 1, 1], the lowest +/// priority segment will hold 50% of the items whereas the other +/// two higher priority segments will hold 25% each. +/// +/// On insertion, a priority is used as an Insertion Point. E.g. a pri-2 +/// region will be inserted into the third highest priority segment. After +/// the insertion is completed, we will trigger rebalance, where this +/// region may be moved to below the insertion point, if the segment it +/// was originally inserted into had exceeded the size allowed by its ratio. +/// +/// Our rebalancing scheme allows the lowest priority segment to grow beyond +/// its ratio allows for, since there is no lower segment to move into. +/// +/// Also note that rebalancing is also triggered after an eviction. +/// +/// The rate of inserting new regions look like the following: +/// Pri-2 --- +/// \ +/// Pri-1 ------- +/// \ +/// Pri-0 ------------ +/// When pri-2 exceeds its ratio, it effectively downgrades the oldest region in +/// pri-2 to pri-1, and that region is now pushed down at the combined rate of +/// (new pri-1 regions + new pri-2 regions), so effectively it gets evicted out +/// of the system faster once it is beyond the pri-2 segment ratio. Segment +/// ratio is put in place to prevent the lower segments getting so small a +/// portion of the flash device. +#[derive(Debug)] +pub struct SegmentedFifo +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + // Note: All queue share the same dlist link. + segments: Vec>, + + config: SegmentedFifoConfig, + total_ratio: usize, + + len: usize, + + adapter: A, +} + +impl Drop for SegmentedFifo +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + fn drop(&mut self) { + let mut to_remove = vec![]; + for ptr in self.iter() { + to_remove.push(ptr.clone()); + } + for ptr in to_remove { + self.remove(&ptr); + } + } +} + +impl SegmentedFifo +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + pub fn new(config: SegmentedFifoConfig) -> Self { + let segments = (0..config.segment_ratios.len()).map(|_| Dlist::new()).collect_vec(); + let total_ratio = config.segment_ratios.iter().sum(); + + Self { + segments, + + config, + total_ratio, + + len: 0, + + adapter: A::new(), + } + } + + fn insert(&mut self, ptr: A::Pointer) { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::into_ptr(ptr) as *mut _); + let mut link = self.adapter.item2link(item); + + assert!(!link.as_ref().is_linked()); + + let priority = *self.adapter.item2priority(item).as_ref(); + link.as_mut().priority = priority.into(); + + self.segments[priority.into()].push_back(link); + + self.rebalance(); + + self.len += 1; + } + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + unsafe { + let item = NonNull::new_unchecked(A::Pointer::as_ptr(ptr) as *mut _); + let link = self.adapter.item2link(item); + + assert!(link.as_ref().is_linked()); + + let priority = link.as_ref().priority; + self.segments[priority] + .iter_mut_from_raw(link.as_ref().link_queue.raw()) + .remove() + .unwrap(); + + self.rebalance(); + + self.len -= 1; + + A::Pointer::from_ptr(item.as_ptr()) + } + } + + fn access(&mut self, _ptr: &A::Pointer) {} + + fn len(&self) -> usize { + self.len + } + + fn rebalance(&mut self) { + unsafe { + let total: usize = self.segments.iter().map(|queue| queue.len()).sum(); + + // Rebalance from highest-pri segment to lowest-pri segment. This means the + // lowest-pri segment can grow to far larger than its ratio suggests. This + // is okay, as we only need higher-pri segments for items that are deemed + // important. + // e.g. {[a, b, c], [d], [e]} is a valid state for a SFIFO with 3 segments + // and a segment ratio of [1, 1, 1] + for high in (1..self.segments.len()).rev() { + let low = high - 1; + let limit = (total as f64 * self.config.segment_ratios[high] as f64 / self.total_ratio as f64) as usize; + while self.segments[high].len() > limit { + let mut link = self.segments[high].pop_front().unwrap(); + link.as_mut().priority = low; + self.segments[low].push_back(link); + } + } + } + } + + fn iter(&self) -> SegmentedFifoIter { + let mut iter_segments = self.segments.iter().map(|queue| queue.iter()).collect_vec(); + + iter_segments.iter_mut().for_each(|iter| iter.front()); + + SegmentedFifoIter { + sfifo: self, + iter_segments, + segment: 0, + + ptr: ManuallyDrop::new(None), + } + } +} + +pub struct SegmentedFifoIter<'a, A> +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + sfifo: &'a SegmentedFifo, + iter_segments: Vec>, + segment: usize, + + ptr: ManuallyDrop>, +} + +impl<'a, A> SegmentedFifoIter<'a, A> +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + unsafe fn update_ptr(&mut self, link: NonNull) { + std::mem::forget(self.ptr.take()); + + let item = self.sfifo.adapter.link2item(link); + let ptr = A::Pointer::from_ptr(item.as_ptr()); + self.ptr = ManuallyDrop::new(Some(ptr)); + } + + unsafe fn ptr(&self) -> Option<&'a A::Pointer> { + if self.ptr.is_none() { + return None; + } + let ptr = self.ptr.as_ref().unwrap(); + let raw = ptr as *const A::Pointer; + Some(&*raw) + } +} + +impl<'a, A> Iterator for SegmentedFifoIter<'a, A> +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + type Item = &'a A::Pointer; + + fn next(&mut self) -> Option { + unsafe { + let mut link = None; + while self.segment < self.iter_segments.len() { + match self.iter_segments[self.segment].get().map(|l| l.raw()) { + Some(l) => { + self.iter_segments[self.segment].next(); + link = Some(l); + break; + } + None => self.segment += 1, + } + } + match link { + None => None, + Some(link) => { + self.update_ptr(link); + self.ptr() + } + } + } + } +} + +// unsafe impl `Send + Sync` for structs with `NonNull` usage + +unsafe impl Send for SegmentedFifo +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ +} + +unsafe impl Sync for SegmentedFifo +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ +} + +unsafe impl Send for SegmentedFifoLink {} + +unsafe impl Sync for SegmentedFifoLink {} + +unsafe impl<'a, A> Send for SegmentedFifoIter<'a, A> +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ +} + +unsafe impl<'a, A> Sync for SegmentedFifoIter<'a, A> +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ +} + +impl EvictionPolicy for SegmentedFifo +where + A: Adapter + PriorityAdapter, + A::Pointer: Clone, +{ + type Adapter = A; + type Config = SegmentedFifoConfig; + + fn new(config: Self::Config) -> Self { + Self::new(config) + } + + fn insert(&mut self, ptr: A::Pointer) { + self.insert(ptr) + } + + fn remove(&mut self, ptr: &A::Pointer) -> A::Pointer { + self.remove(ptr) + } + + fn access(&mut self, ptr: &A::Pointer) { + self.access(ptr) + } + + fn len(&self) -> usize { + self.len() + } + + fn iter(&self) -> impl Iterator { + self.iter() + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use itertools::Itertools; + + use super::*; + use crate::{eviction::EvictionPolicyExt, priority_adapter}; + + #[derive(Debug)] + struct SegmentedFifoItem { + link: SegmentedFifoLink, + key: u64, + priority: usize, + } + + impl SegmentedFifoItem { + fn new(key: u64, priority: usize) -> Self { + Self { + link: SegmentedFifoLink::default(), + key, + priority, + } + } + } + + intrusive_adapter! { SegmentedFifoItemAdapter = Arc: SegmentedFifoItem { link: SegmentedFifoLink } } + priority_adapter! { SegmentedFifoItemAdapter = SegmentedFifoItem { priority: usize } } + + #[test] + fn test_sfifo_simple() { + let config = SegmentedFifoConfig { + segment_ratios: vec![6, 3, 1], + }; + let mut fifo = SegmentedFifo::::new(config); + + let mut items = vec![]; + + // see comments in `rebalance` + // inner: [[0, 1, 2, 3, 4, 5, 6], [7, 8], [9]] + for key in 0..10 { + let item = Arc::new(SegmentedFifoItem::new(key, 2)); + fifo.push(item.clone()); + items.push(item); + } + let v = fifo.iter().map(|item| item.key).collect_vec(); + assert_eq!(v, (0..10).collect_vec()); + let lens = fifo.segments.iter().map(|queue| queue.len()).collect_vec(); + assert_eq!(lens, vec![7, 2, 1]); + + // inner: [[0, 1, 2, 3, 4, 5, 6], [7, 8, 10], [9]] + let item = Arc::new(SegmentedFifoItem::new(10, 1)); + fifo.push(item.clone()); + items.push(item); + let v = fifo.iter().map(|item| item.key).collect_vec(); + assert_eq!(v, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 9]); + + // inner: [[0, 1, 2, 3, 4, 5, 6], [7, 10], [9]] + fifo.remove(&items[8]); + let v = fifo.iter().map(|item| item.key).collect_vec(); + assert_eq!(v, vec![0, 1, 2, 3, 4, 5, 6, 7, 10, 9]); + + drop(fifo); + + for item in items { + assert_eq!(Arc::strong_count(&item), 1); + } + } +} diff --git a/foyer-intrusive/src/lib.rs b/foyer-intrusive/src/lib.rs new file mode 100644 index 00000000..5fde39b0 --- /dev/null +++ b/foyer-intrusive/src/lib.rs @@ -0,0 +1,48 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#![allow(clippy::new_without_default)] + +pub use memoffset::offset_of; + +/// Unsafe macro to get a raw pointer to an outer object from a pointer to one +/// of its fields. +/// +/// # Examples +/// +/// ``` +/// use foyer_intrusive::container_of; +/// +/// struct S { x: u32, y: u32 }; +/// let container = S { x: 1, y: 2 }; +/// let field = &container.x; +/// let container2: *const S = unsafe { container_of!(field, S, x) }; +/// assert_eq!(&container as *const S, container2); +/// ``` +/// +/// # Safety +/// +/// This is unsafe because it assumes that the given expression is a valid +/// pointer to the specified field of some container type. +#[macro_export] +macro_rules! container_of { + ($ptr:expr, $container:path, $field:ident) => { + ($ptr as *const _ as *const u8).sub($crate::offset_of!($container, $field)) as *const $container + }; +} + +pub mod collections; +pub mod core; +pub mod eviction; diff --git a/foyer-memory/Cargo.toml b/foyer-memory/Cargo.toml new file mode 100644 index 00000000..2eb1ef52 --- /dev/null +++ b/foyer-memory/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "foyer-memory" +version = "0.2.0" +edition = "2021" +authors = ["MrCroxx "] +description = "memory cache for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +ahash = "0.8" +bitflags = "2" +cmsketch = "0.2" +crossbeam = "0.8" +foyer-intrusive = { version = "0.4", path = "../foyer-intrusive" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +futures = "0.3" +hashbrown = "0.14" +itertools = "0.12" +libc = "0.2" +parking_lot = "0.12" +tokio = { workspace = true } + +[dev-dependencies] +bytesize = "1" +clap = { version = "4", features = ["derive"] } +hdrhistogram = "7" +moka = { version = "0", features = ["sync"] } +rand = "0.8" +rand_mt = "4.2.1" +tempfile = "3" +zipf = "7.0.1" + +[features] +deadlock = ["parking_lot/deadlock_detection"] + +[[bench]] +name = "bench_hit_ratio" +harness = false + +[[bench]] +name = "bench_dynamic_dispatch" +harness = false diff --git a/foyer-memory/benches/bench_dynamic_dispatch.rs b/foyer-memory/benches/bench_dynamic_dispatch.rs new file mode 100644 index 00000000..1e92d5c9 --- /dev/null +++ b/foyer-memory/benches/bench_dynamic_dispatch.rs @@ -0,0 +1,121 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use rand::{distributions::Alphanumeric, thread_rng, Rng}; + +struct T +where + F: Fn(&str) -> usize, +{ + f1: F, + f2: Box usize>, + f3: Arc usize>, +} + +fn rand_string(len: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +fn bench_static_dispatch(t: &T, loops: usize) -> Duration +where + F: Fn(&str) -> usize, +{ + let mut dur = Duration::default(); + for _ in 0..loops { + let s = rand_string(thread_rng().gen_range(0..100)); + let now = Instant::now(); + let _ = (t.f1)(&s); + dur += now.elapsed(); + } + Duration::from_nanos((dur.as_nanos() as usize / loops) as _) +} + +fn bench_box_dynamic_dispatch(t: &T, loops: usize) -> Duration +where + F: Fn(&str) -> usize, +{ + let mut dur = Duration::default(); + for _ in 0..loops { + let s = rand_string(thread_rng().gen_range(0..100)); + let now = Instant::now(); + let _ = (t.f3)(&s); + dur += now.elapsed(); + } + Duration::from_nanos((dur.as_nanos() as usize / loops) as _) +} + +fn bench_arc_dynamic_dispatch(t: &T, loops: usize) -> Duration +where + F: Fn(&str) -> usize, +{ + let mut dur = Duration::default(); + for _ in 0..loops { + let s = rand_string(thread_rng().gen_range(0..100)); + let now = Instant::now(); + let _ = (t.f2)(&s); + dur += now.elapsed(); + } + Duration::from_nanos((dur.as_nanos() as usize / loops) as _) +} + +fn main() { + let t = T { + f1: |s: &str| s.len(), + f2: Box::new(|s: &str| s.len()), + f3: Arc::new(|s: &str| s.len()), + }; + + let _ = T { + f1: |s: &str| s.len(), + f2: Box::new(|s: &str| s.len() + 1), + f3: Arc::new(|s: &str| s.len() + 1), + }; + + let _ = T { + f1: |s: &str| s.len(), + f2: Box::new(|s: &str| s.len() + 2), + f3: Arc::new(|s: &str| s.len() + 2), + }; + + let _ = T { + f1: |s: &str| s.len(), + f2: Box::new(|s: &str| s.len() + 3), + f3: Arc::new(|s: &str| s.len() + 3), + }; + + for loops in [100_000, 1_000_000, 10_000_000] { + println!(); + + println!(" statis - {} loops : {:?}", loops, bench_static_dispatch(&t, loops)); + println!( + "box dynamic - {} loops : {:?}", + loops, + bench_box_dynamic_dispatch(&t, loops) + ); + println!( + "arc dynamic - {} loops : {:?}", + loops, + bench_arc_dynamic_dispatch(&t, loops) + ); + } +} diff --git a/foyer-memory/benches/bench_hit_ratio.rs b/foyer-memory/benches/bench_hit_ratio.rs new file mode 100644 index 00000000..3cb51fa7 --- /dev/null +++ b/foyer-memory/benches/bench_hit_ratio.rs @@ -0,0 +1,226 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use ahash::RandomState; +use foyer_memory::{ + Cache, DefaultCacheEventListener, FifoCacheConfig, FifoConfig, LfuCacheConfig, LfuConfig, LruCacheConfig, + LruConfig, S3FifoCacheConfig, S3FifoConfig, +}; +use rand::{distributions::Distribution, thread_rng}; + +type CacheKey = String; +type CacheValue = (); + +const ITEMS: usize = 10_000; +const ITERATIONS: usize = 5_000_000; + +const SHARDS: usize = 1; +const OBJECT_POOL_CAPACITY: usize = 16; +/* +inspired by pingora/tinyufo/benches/bench_hit_ratio.rs +cargo bench --bench bench_hit_ratio + +zif_exp, cache_size fifo lru lfu s3fifo moka +0.90, 0.005 16.24% 19.22% 32.37% 32.39% 33.50% +0.90, 0.01 22.55% 26.20% 38.54% 39.20% 37.92% +0.90, 0.05 41.05% 45.56% 55.37% 56.63% 55.25% +0.90, 0.1 51.06% 55.68% 63.82% 65.06% 64.20% +0.90, 0.25 66.81% 71.17% 76.21% 77.26% 77.12% +1.00, 0.005 26.62% 31.10% 44.16% 44.15% 45.62% +1.00, 0.01 34.38% 39.17% 50.63% 51.29% 50.72% +1.00, 0.05 54.04% 58.76% 66.79% 67.85% 66.89% +1.00, 0.1 63.15% 67.60% 73.93% 74.92% 74.38% +1.00, 0.25 76.18% 79.95% 83.63% 84.39% 84.38% +1.05, 0.005 32.67% 37.71% 50.26% 50.21% 51.85% +1.05, 0.01 40.97% 46.10% 56.74% 57.40% 57.09% +1.05, 0.05 60.44% 65.03% 72.04% 73.02% 72.28% +1.05, 0.1 68.93% 73.12% 78.49% 79.37% 79.00% +1.05, 0.25 80.38% 83.73% 86.78% 87.42% 87.41% +1.10, 0.005 39.02% 44.50% 56.26% 56.20% 57.90% +1.10, 0.01 47.60% 52.93% 62.61% 63.24% 63.05% +1.10, 0.05 66.59% 70.95% 76.92% 77.76% 77.27% +1.10, 0.1 74.24% 78.07% 82.54% 83.28% 83.00% +1.10, 0.25 84.18% 87.10% 89.57% 90.06% 90.08% +1.50, 0.005 81.17% 85.27% 88.90% 89.10% 89.89% +1.50, 0.01 86.91% 89.87% 92.25% 92.56% 92.79% +1.50, 0.05 94.77% 96.04% 96.96% 97.10% 97.07% +1.50, 0.1 96.65% 97.50% 98.06% 98.14% 98.15% +1.50, 0.25 98.36% 98.81% 99.04% 99.06% 99.09% +*/ +fn cache_hit(cache: Arc>, keys: Arc>) -> f64 { + let mut hit = 0; + for key in keys.iter() { + let value = cache.get(key); + if value.is_some() { + hit += 1; + } else { + cache.insert(key.clone(), (), 1); + } + } + hit as f64 / ITERATIONS as f64 +} + +fn moka_cache_hit(cache: &moka::sync::Cache, keys: &[String]) -> f64 { + let mut hit = 0; + for key in keys.iter() { + let value = cache.get(key); + if value.is_some() { + hit += 1; + } else { + cache.insert(key.clone(), ()); + } + } + hit as f64 / ITERATIONS as f64 +} + +fn new_fifo_cache(capacity: usize) -> Arc> { + let config = FifoCacheConfig { + capacity, + shards: SHARDS, + eviction_config: FifoConfig {}, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::fifo(config)) +} + +fn new_lru_cache(capacity: usize) -> Arc> { + let config = LruCacheConfig { + capacity, + shards: SHARDS, + eviction_config: LruConfig { + high_priority_pool_ratio: 0.1, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::lru(config)) +} + +fn new_lfu_cache(capacity: usize) -> Arc> { + let config = LfuCacheConfig { + capacity, + shards: SHARDS, + eviction_config: LfuConfig { + window_capacity_ratio: 0.1, + protected_capacity_ratio: 0.8, + cmsketch_eps: 0.001, + cmsketch_confidence: 0.9, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::lfu(config)) +} + +fn new_s3fifo_cache(capacity: usize) -> Arc> { + let config = S3FifoCacheConfig { + capacity, + shards: SHARDS, + eviction_config: S3FifoConfig { + small_queue_capacity_ratio: 0.1, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::s3fifo(config)) +} + +fn bench_one(zif_exp: f64, cache_size_percent: f64) { + print!("{zif_exp:.2}, {cache_size_percent:4}\t\t\t"); + let mut rng = thread_rng(); + let zipf = zipf::ZipfDistribution::new(ITEMS, zif_exp).unwrap(); + + let cache_size = (ITEMS as f64 * cache_size_percent) as usize; + + let fifo_cache = new_fifo_cache(cache_size); + let lru_cache = new_lru_cache(cache_size); + let lfu_cache = new_lfu_cache(cache_size); + let s3fifo_cache = new_s3fifo_cache(cache_size); + let moka_cache = moka::sync::Cache::new(cache_size as u64); + + let mut keys = Vec::with_capacity(ITERATIONS); + for _ in 0..ITERATIONS { + let key = zipf.sample(&mut rng).to_string(); + keys.push(key.clone()); + } + + let keys = Arc::new(keys); + + // Use multiple threads to simulate concurrent read-through requests. + let fifo_cache_hit_handle = std::thread::spawn({ + let cache = fifo_cache.clone(); + let keys = keys.clone(); + move || cache_hit(cache, keys) + }); + + let lru_cache_hit_handle = std::thread::spawn({ + let cache = lru_cache.clone(); + let keys = keys.clone(); + move || cache_hit(cache, keys) + }); + + let lfu_cache_hit_handle = std::thread::spawn({ + let cache = lfu_cache.clone(); + let keys = keys.clone(); + move || cache_hit(cache, keys) + }); + + let s3fifo_cache_hit_handle = std::thread::spawn({ + let cache = s3fifo_cache.clone(); + let keys = keys.clone(); + move || cache_hit(cache, keys) + }); + + let moka_cache_hit_handle = std::thread::spawn({ + let cache = moka_cache.clone(); + let keys = keys.clone(); + move || moka_cache_hit(&cache, &keys) + }); + + let fifo_hit_ratio = fifo_cache_hit_handle.join().unwrap(); + let lru_hit_ratio = lru_cache_hit_handle.join().unwrap(); + let lfu_hit_ratio = lfu_cache_hit_handle.join().unwrap(); + let s3fifo_hit_ratio = s3fifo_cache_hit_handle.join().unwrap(); + let moka_hit_ratio = moka_cache_hit_handle.join().unwrap(); + + print!("{:.2}%\t\t", fifo_hit_ratio * 100.0); + print!("{:.2}%\t\t", lru_hit_ratio * 100.0); + print!("{:.2}%\t\t", lfu_hit_ratio * 100.0); + print!("{:.2}%\t\t", s3fifo_hit_ratio * 100.0); + println!("{:.2}%", moka_hit_ratio * 100.0); +} + +fn bench_zipf_hit() { + println!("zif_exp, cache_size\t\tfifo\t\tlru\t\tlfu\t\ts3fifo\t\tmoka"); + for zif_exp in [0.9, 1.0, 1.05, 1.1, 1.5] { + for cache_capacity in [0.005, 0.01, 0.05, 0.1, 0.25] { + bench_one(zif_exp, cache_capacity); + } + } +} + +fn main() { + bench_zipf_hit(); +} diff --git a/foyer-memory/fuzz/Cargo.toml b/foyer-memory/fuzz/Cargo.toml new file mode 100644 index 00000000..206453d6 --- /dev/null +++ b/foyer-memory/fuzz/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "foyer-memory-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[dependencies] +ahash = "0.8" +arbitrary = { version = "1", features = ["derive"] } +foyer-memory = { path = ".." } +foyer-workspace-hack = { version = "0.4", path = "../../foyer-workspace-hack" } +libfuzzer-sys = "0.4" + +[package.metadata] +cargo-fuzz = true + +[[bin]] +name = "cache_fuzz" +path = "fuzz_targets/cache_fuzz.rs" +test = false +doc = false +bench = false diff --git a/foyer-memory/fuzz/fuzz_targets/cache_fuzz.rs b/foyer-memory/fuzz/fuzz_targets/cache_fuzz.rs new file mode 100644 index 00000000..f3c47405 --- /dev/null +++ b/foyer-memory/fuzz/fuzz_targets/cache_fuzz.rs @@ -0,0 +1,145 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_main] + +use std::sync::Arc; + +use ahash::RandomState; +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; + +use foyer_memory::{ + Cache, DefaultCacheEventListener, FifoCacheConfig, FifoConfig, LfuCacheConfig, LfuConfig, LruCacheConfig, + LruConfig, S3FifoCacheConfig, S3FifoConfig, +}; + +type CacheKey = u8; +type CacheValue = u8; +const SHARDS: usize = 1; +const OBJECT_POOL_CAPACITY: usize = 16; + +fn new_fifo_cache(capacity: usize) -> Arc> { + let config = FifoCacheConfig { + capacity, + shards: SHARDS, + eviction_config: FifoConfig {}, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::fifo(config)) +} + +fn new_lru_cache(capacity: usize) -> Arc> { + let config = LruCacheConfig { + capacity, + shards: SHARDS, + eviction_config: LruConfig { + high_priority_pool_ratio: 0.1, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::lru(config)) +} + +fn new_lfu_cache(capacity: usize) -> Arc> { + let config = LfuCacheConfig { + capacity, + shards: SHARDS, + eviction_config: LfuConfig { + window_capacity_ratio: 0.1, + protected_capacity_ratio: 0.8, + cmsketch_eps: 0.001, + cmsketch_confidence: 0.9, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::lfu(config)) +} + +fn new_s3fifo_cache(capacity: usize) -> Arc> { + let config = S3FifoCacheConfig { + capacity, + shards: SHARDS, + eviction_config: S3FifoConfig { + small_queue_capacity_ratio: 0.1, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + + Arc::new(Cache::s3fifo(config)) +} + +#[derive(Debug, Arbitrary)] +enum Op { + Insert(CacheKey, CacheValue, usize), + Get(CacheKey), + Remove(CacheKey), + Clear, +} + +#[derive(Debug, Arbitrary)] +enum CacheType { + Fifo, + Lru, + Lfu, + S3Fifo, +} + +#[derive(Debug, Arbitrary)] +struct Input { + capacity: usize, + cache_type: CacheType, + operations: Vec, +} + +fn create_cache(cache_type: CacheType, capacity: usize) -> Arc> { + match cache_type { + CacheType::Fifo => new_fifo_cache(capacity), + CacheType::Lru => new_lru_cache(capacity), + CacheType::Lfu => new_lfu_cache(capacity), + CacheType::S3Fifo => new_s3fifo_cache(capacity), + } +} + +fuzz_target!(|data: Input| { + let cache = create_cache(data.cache_type, data.capacity); + + for op in data.operations { + match op { + Op::Insert(k, v, size) => { + cache.insert(k, v, size); + } + Op::Get(k) => { + let _ = cache.get(&k); + } + Op::Remove(k) => { + cache.remove(&k); + } + Op::Clear => { + cache.clear(); + } + } + } +}); diff --git a/foyer-memory/src/cache.rs b/foyer-memory/src/cache.rs new file mode 100644 index 00000000..277f858f --- /dev/null +++ b/foyer-memory/src/cache.rs @@ -0,0 +1,675 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + borrow::Borrow, + hash::{BuildHasher, Hash}, + ops::Deref, + sync::Arc, +}; + +use ahash::RandomState; +use futures::{Future, FutureExt}; +use tokio::sync::oneshot; + +use crate::{ + context::CacheContext, + eviction::{ + fifo::{Fifo, FifoHandle}, + lfu::{Lfu, LfuHandle}, + lru::{Lru, LruHandle}, + s3fifo::{S3Fifo, S3FifoHandle}, + }, + generic::{CacheConfig, GenericCache, GenericCacheEntry, GenericEntry}, + indexer::HashTableIndexer, + listener::{CacheEventListener, DefaultCacheEventListener}, + metrics::Metrics, + Key, Value, +}; + +pub type FifoCache, S = RandomState> = + GenericCache, HashTableIndexer>, L, S>; +pub type FifoCacheConfig, S = RandomState> = + CacheConfig, L, S>; +pub type FifoCacheEntry, S = RandomState> = + GenericCacheEntry, HashTableIndexer>, L, S>; +pub type FifoEntry, S = RandomState> = + GenericEntry, HashTableIndexer>, L, S, ER>; + +pub type LruCache, S = RandomState> = + GenericCache, HashTableIndexer>, L, S>; +pub type LruCacheConfig, S = RandomState> = + CacheConfig, L, S>; +pub type LruCacheEntry, S = RandomState> = + GenericCacheEntry, HashTableIndexer>, L, S>; +pub type LruEntry, S = RandomState> = + GenericEntry, HashTableIndexer>, L, S, ER>; + +pub type LfuCache, S = RandomState> = + GenericCache, HashTableIndexer>, L, S>; +pub type LfuCacheConfig, S = RandomState> = + CacheConfig, L, S>; +pub type LfuCacheEntry, S = RandomState> = + GenericCacheEntry, HashTableIndexer>, L, S>; +pub type LfuEntry, S = RandomState> = + GenericEntry, HashTableIndexer>, L, S, ER>; + +pub type S3FifoCache, S = RandomState> = + GenericCache, HashTableIndexer>, L, S>; +pub type S3FifoCacheConfig, S = RandomState> = + CacheConfig, L, S>; +pub type S3FifoCacheEntry, S = RandomState> = + GenericCacheEntry, HashTableIndexer>, L, S>; +pub type S3FifoEntry, S = RandomState> = + GenericEntry, HashTableIndexer>, L, S, ER>; + +pub enum CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + Fifo(FifoCacheEntry), + Lru(LruCacheEntry), + Lfu(LfuCacheEntry), + S3Fifo(S3FifoCacheEntry), +} + +impl Clone for CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn clone(&self) -> Self { + match self { + Self::Fifo(entry) => Self::Fifo(entry.clone()), + Self::Lru(entry) => Self::Lru(entry.clone()), + Self::Lfu(entry) => Self::Lfu(entry.clone()), + Self::S3Fifo(entry) => Self::S3Fifo(entry.clone()), + } + } +} + +impl Deref for CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + type Target = V; + + fn deref(&self) -> &Self::Target { + match self { + CacheEntry::Fifo(entry) => entry.deref(), + CacheEntry::Lru(entry) => entry.deref(), + CacheEntry::Lfu(entry) => entry.deref(), + CacheEntry::S3Fifo(entry) => entry.deref(), + } + } +} + +impl From> for CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: FifoCacheEntry) -> Self { + Self::Fifo(entry) + } +} + +impl From> for CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: LruCacheEntry) -> Self { + Self::Lru(entry) + } +} + +impl From> for CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: LfuCacheEntry) -> Self { + Self::Lfu(entry) + } +} + +impl From> for CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: S3FifoCacheEntry) -> Self { + Self::S3Fifo(entry) + } +} + +impl CacheEntry +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn key(&self) -> &K { + match self { + CacheEntry::Fifo(entry) => entry.key(), + CacheEntry::Lru(entry) => entry.key(), + CacheEntry::Lfu(entry) => entry.key(), + CacheEntry::S3Fifo(entry) => entry.key(), + } + } + + pub fn value(&self) -> &V { + match self { + CacheEntry::Fifo(entry) => entry.value(), + CacheEntry::Lru(entry) => entry.value(), + CacheEntry::Lfu(entry) => entry.value(), + CacheEntry::S3Fifo(entry) => entry.value(), + } + } + + pub fn context(&self) -> CacheContext { + match self { + CacheEntry::Fifo(entry) => entry.context().clone().into(), + CacheEntry::Lru(entry) => entry.context().clone().into(), + CacheEntry::Lfu(entry) => entry.context().clone().into(), + CacheEntry::S3Fifo(entry) => entry.context().clone().into(), + } + } + + pub fn charge(&self) -> usize { + match self { + CacheEntry::Fifo(entry) => entry.charge(), + CacheEntry::Lru(entry) => entry.charge(), + CacheEntry::Lfu(entry) => entry.charge(), + CacheEntry::S3Fifo(entry) => entry.charge(), + } + } + + pub fn refs(&self) -> usize { + match self { + CacheEntry::Fifo(entry) => entry.refs(), + CacheEntry::Lru(entry) => entry.refs(), + CacheEntry::Lfu(entry) => entry.refs(), + CacheEntry::S3Fifo(entry) => entry.refs(), + } + } +} + +pub enum Cache, S = RandomState> +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + Fifo(Arc>), + Lru(Arc>), + Lfu(Arc>), + S3Fifo(Arc>), +} + +impl Clone for Cache +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn clone(&self) -> Self { + match self { + Self::Fifo(cache) => Self::Fifo(cache.clone()), + Self::Lru(cache) => Self::Lru(cache.clone()), + Self::Lfu(cache) => Self::Lfu(cache.clone()), + Self::S3Fifo(cache) => Self::S3Fifo(cache.clone()), + } + } +} + +impl Cache +where + K: Key, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn fifo(config: FifoCacheConfig) -> Self { + Self::Fifo(Arc::new(GenericCache::new(config))) + } + + pub fn lru(config: LruCacheConfig) -> Self { + Self::Lru(Arc::new(GenericCache::new(config))) + } + + pub fn lfu(config: LfuCacheConfig) -> Self { + Self::Lfu(Arc::new(GenericCache::new(config))) + } + + pub fn s3fifo(config: S3FifoCacheConfig) -> Self { + Self::S3Fifo(Arc::new(GenericCache::new(config))) + } + + pub fn insert(&self, key: K, value: V, charge: usize) -> CacheEntry { + match self { + Cache::Fifo(cache) => cache.insert(key, value, charge).into(), + Cache::Lru(cache) => cache.insert(key, value, charge).into(), + Cache::Lfu(cache) => cache.insert(key, value, charge).into(), + Cache::S3Fifo(cache) => cache.insert(key, value, charge).into(), + } + } + + pub fn insert_with_context( + &self, + key: K, + value: V, + charge: usize, + context: CacheContext, + ) -> CacheEntry { + match self { + Cache::Fifo(cache) => cache.insert_with_context(key, value, charge, context).into(), + Cache::Lru(cache) => cache.insert_with_context(key, value, charge, context).into(), + Cache::Lfu(cache) => cache.insert_with_context(key, value, charge, context).into(), + Cache::S3Fifo(cache) => cache.insert_with_context(key, value, charge, context).into(), + } + } + + pub fn remove(&self, key: &Q) + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + match self { + Cache::Fifo(cache) => cache.remove(key), + Cache::Lru(cache) => cache.remove(key), + Cache::Lfu(cache) => cache.remove(key), + Cache::S3Fifo(cache) => cache.remove(key), + } + } + + pub fn get(&self, key: &Q) -> Option> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + match self { + Cache::Fifo(cache) => cache.get(key).map(CacheEntry::from), + Cache::Lru(cache) => cache.get(key).map(CacheEntry::from), + Cache::Lfu(cache) => cache.get(key).map(CacheEntry::from), + Cache::S3Fifo(cache) => cache.get(key).map(CacheEntry::from), + } + } + + pub fn contains(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + match self { + Cache::Fifo(cache) => cache.contains(key), + Cache::Lru(cache) => cache.contains(key), + Cache::Lfu(cache) => cache.contains(key), + Cache::S3Fifo(cache) => cache.contains(key), + } + } + + pub fn clear(&self) { + match self { + Cache::Fifo(cache) => cache.clear(), + Cache::Lru(cache) => cache.clear(), + Cache::Lfu(cache) => cache.clear(), + Cache::S3Fifo(cache) => cache.clear(), + } + } + + pub fn capacity(&self) -> usize { + match self { + Cache::Fifo(cache) => cache.capacity(), + Cache::Lru(cache) => cache.capacity(), + Cache::Lfu(cache) => cache.capacity(), + Cache::S3Fifo(cache) => cache.capacity(), + } + } + + pub fn usage(&self) -> usize { + match self { + Cache::Fifo(cache) => cache.usage(), + Cache::Lru(cache) => cache.usage(), + Cache::Lfu(cache) => cache.usage(), + Cache::S3Fifo(cache) => cache.usage(), + } + } + + pub fn metrics(&self) -> &Metrics { + match self { + Cache::Fifo(cache) => cache.metrics(), + Cache::Lru(cache) => cache.metrics(), + Cache::Lfu(cache) => cache.metrics(), + Cache::S3Fifo(cache) => cache.metrics(), + } + } +} + +pub enum Entry, S = RandomState> +where + K: Key + Clone, + V: Value, + ER: std::error::Error, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + Fifo(FifoEntry), + Lru(LruEntry), + Lfu(LfuEntry), + S3Fifo(S3FifoEntry), +} + +impl From> for Entry +where + K: Key + Clone, + V: Value, + ER: std::error::Error, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: FifoEntry) -> Self { + Self::Fifo(entry) + } +} + +impl From> for Entry +where + K: Key + Clone, + V: Value, + ER: std::error::Error, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: LruEntry) -> Self { + Self::Lru(entry) + } +} + +impl From> for Entry +where + K: Key + Clone, + V: Value, + ER: std::error::Error, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: LfuEntry) -> Self { + Self::Lfu(entry) + } +} + +impl From> for Entry +where + K: Key + Clone, + V: Value, + ER: std::error::Error, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn from(entry: S3FifoEntry) -> Self { + Self::S3Fifo(entry) + } +} + +impl Future for Entry +where + K: Key + Clone, + V: Value, + ER: std::error::Error + From, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + type Output = std::result::Result, ER>; + + fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll { + match &mut *self { + Entry::Fifo(entry) => entry.poll_unpin(cx).map(|res| res.map(CacheEntry::from)), + Entry::Lru(entry) => entry.poll_unpin(cx).map(|res| res.map(CacheEntry::from)), + Entry::Lfu(entry) => entry.poll_unpin(cx).map(|res| res.map(CacheEntry::from)), + Entry::S3Fifo(entry) => entry.poll_unpin(cx).map(|res| res.map(CacheEntry::from)), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntryState { + Hit, + Wait, + Miss, +} + +impl Entry +where + K: Key + Clone, + V: Value, + ER: std::error::Error, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn state(&self) -> EntryState { + match self { + Entry::Fifo(FifoEntry::Hit(_)) + | Entry::Lru(LruEntry::Hit(_)) + | Entry::Lfu(LfuEntry::Hit(_)) + | Entry::S3Fifo(S3FifoEntry::Hit(_)) => EntryState::Hit, + Entry::Fifo(FifoEntry::Wait(_)) + | Entry::Lru(LruEntry::Wait(_)) + | Entry::Lfu(LfuEntry::Wait(_)) + | Entry::S3Fifo(S3FifoEntry::Wait(_)) => EntryState::Wait, + Entry::Fifo(FifoEntry::Miss(_)) + | Entry::Lru(LruEntry::Miss(_)) + | Entry::Lfu(LfuEntry::Miss(_)) + | Entry::S3Fifo(S3FifoEntry::Miss(_)) => EntryState::Miss, + Entry::Fifo(FifoEntry::Invalid) + | Entry::Lru(LruEntry::Invalid) + | Entry::Lfu(LfuEntry::Invalid) + | Entry::S3Fifo(S3FifoEntry::Invalid) => unreachable!(), + } + } +} + +impl Cache +where + K: Key + Clone, + V: Value, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn entry(&self, key: K, f: F) -> Entry + where + F: FnOnce() -> FU, + FU: Future> + Send + 'static, + ER: std::error::Error + Send + 'static, + { + match self { + Cache::Fifo(cache) => Entry::from(cache.entry(key, f)), + Cache::Lru(cache) => Entry::from(cache.entry(key, f)), + Cache::Lfu(cache) => Entry::from(cache.entry(key, f)), + Cache::S3Fifo(cache) => Entry::from(cache.entry(key, f)), + } + } +} + +#[cfg(test)] +mod tests { + use std::{ops::Range, time::Duration}; + + use futures::future::join_all; + use itertools::Itertools; + use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng}; + + use super::*; + use crate::{eviction::s3fifo::S3FifoConfig, FifoConfig, LfuConfig, LruConfig}; + + const CAPACITY: usize = 100; + const SHARDS: usize = 4; + const OBJECT_POOL_CAPACITY: usize = 64; + const RANGE: Range = 0..1000; + const OPS: usize = 10000; + const CONCURRENCY: usize = 8; + + fn fifo() -> Cache { + Cache::fifo(FifoCacheConfig { + capacity: CAPACITY, + shards: SHARDS, + eviction_config: FifoConfig {}, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }) + } + + fn lru() -> Cache { + Cache::lru(LruCacheConfig { + capacity: CAPACITY, + shards: SHARDS, + eviction_config: LruConfig { + high_priority_pool_ratio: 0.1, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }) + } + + fn lfu() -> Cache { + Cache::lfu(LfuCacheConfig { + capacity: CAPACITY, + shards: SHARDS, + eviction_config: LfuConfig { + window_capacity_ratio: 0.1, + protected_capacity_ratio: 0.8, + cmsketch_eps: 0.001, + cmsketch_confidence: 0.9, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }) + } + + fn s3fifo() -> Cache { + Cache::s3fifo(S3FifoCacheConfig { + capacity: CAPACITY, + shards: SHARDS, + eviction_config: S3FifoConfig { + small_queue_capacity_ratio: 0.1, + }, + object_pool_capacity: OBJECT_POOL_CAPACITY, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }) + } + + fn init_cache(cache: &Cache, rng: &mut StdRng) { + let mut v = RANGE.collect_vec(); + v.shuffle(rng); + for i in v { + cache.insert(i, i, 1); + } + } + + async fn operate(cache: &Cache, rng: &mut StdRng) { + let i = rng.gen_range(RANGE); + match rng.gen_range(0..=3) { + 0 => { + let entry = cache.insert(i, i, 1); + assert_eq!(*entry.key(), i); + assert_eq!(entry.key(), entry.value()); + } + 1 => { + if let Some(entry) = cache.get(&i) { + assert_eq!(*entry.key(), i); + assert_eq!(entry.key(), entry.value()); + } + } + 2 => { + cache.remove(&i); + } + 3 => { + let entry = cache + .entry(i, || async move { + tokio::time::sleep(Duration::from_micros(10)).await; + Ok::<_, tokio::sync::oneshot::error::RecvError>((i, 1, CacheContext::Default)) + }) + .await + .unwrap(); + assert_eq!(*entry.key(), i); + assert_eq!(entry.key(), entry.value()); + } + _ => unreachable!(), + } + } + + async fn case(cache: Cache) { + let mut rng = StdRng::seed_from_u64(42); + + init_cache(&cache, &mut rng); + + let handles = (0..CONCURRENCY) + .map(|_| { + let cache = cache.clone(); + let mut rng = rng.clone(); + tokio::spawn(async move { + for _ in 0..OPS { + operate(&cache, &mut rng).await; + } + }) + }) + .collect_vec(); + + join_all(handles).await; + } + + #[tokio::test] + async fn test_fifo_cache() { + case(fifo()).await + } + + #[tokio::test] + async fn test_lru_cache() { + case(lru()).await + } + + #[tokio::test] + async fn test_lfu_cache() { + case(lfu()).await + } + + #[tokio::test] + async fn test_s3fifo_cache() { + case(s3fifo()).await + } +} diff --git a/foyer-memory/src/context.rs b/foyer-memory/src/context.rs new file mode 100644 index 00000000..3ca02c9d --- /dev/null +++ b/foyer-memory/src/context.rs @@ -0,0 +1,31 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CacheContext { + /// The default context shared by all eviction container implementations. + Default, + LruPriorityLow, +} + +impl Default for CacheContext { + fn default() -> Self { + Self::Default + } +} + +/// The overhead of `Context` itself and the conversion should be light. +pub trait Context: From + Into + Send + Sync + 'static + Clone {} + +impl Context for T where T: From + Into + Send + Sync + 'static + Clone {} diff --git a/foyer-memory/src/eviction/fifo.rs b/foyer-memory/src/eviction/fifo.rs new file mode 100644 index 00000000..058cc715 --- /dev/null +++ b/foyer-memory/src/eviction/fifo.rs @@ -0,0 +1,225 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, ptr::NonNull}; + +use foyer_intrusive::{ + collections::dlist::{Dlist, DlistLink}, + intrusive_adapter, +}; + +use crate::{ + eviction::Eviction, + handle::{BaseHandle, Handle}, + CacheContext, +}; + +#[derive(Debug, Clone)] +pub struct FifoContext; + +impl From for FifoContext { + fn from(_: CacheContext) -> Self { + Self + } +} + +impl From for CacheContext { + fn from(_: FifoContext) -> Self { + CacheContext::Default + } +} + +pub struct FifoHandle +where + T: Send + Sync + 'static, +{ + link: DlistLink, + base: BaseHandle, +} + +impl Debug for FifoHandle +where + T: Send + Sync + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FifoHandle").finish() + } +} + +intrusive_adapter! { FifoHandleDlistAdapter = NonNull>: FifoHandle { link: DlistLink } where T: Send + Sync + 'static } + +impl Handle for FifoHandle +where + T: Send + Sync + 'static, +{ + type Data = T; + type Context = FifoContext; + + fn new() -> Self { + Self { + link: DlistLink::default(), + base: BaseHandle::new(), + } + } + + fn init(&mut self, hash: u64, data: Self::Data, charge: usize, context: Self::Context) { + self.base.init(hash, data, charge, context); + } + + fn base(&self) -> &BaseHandle { + &self.base + } + + fn base_mut(&mut self) -> &mut BaseHandle { + &mut self.base + } +} + +#[derive(Debug, Clone)] +pub struct FifoConfig {} + +pub struct Fifo +where + T: Send + Sync + 'static, +{ + queue: Dlist>, +} + +impl Eviction for Fifo +where + T: Send + Sync + 'static, +{ + type Item = FifoHandle; + type Config = FifoConfig; + + unsafe fn new(_capacity: usize, _config: &Self::Config) -> Self + where + Self: Sized, + { + Self { queue: Dlist::new() } + } + + unsafe fn push(&mut self, mut ptr: NonNull) { + self.queue.push_back(ptr); + ptr.as_mut().base_mut().set_in_eviction(true); + } + + unsafe fn pop(&mut self) -> Option> { + self.queue.pop_front().map(|mut ptr| { + ptr.as_mut().base_mut().set_in_eviction(false); + ptr + }) + } + + unsafe fn reinsert(&mut self, _: NonNull) {} + + unsafe fn access(&mut self, _: NonNull) {} + + unsafe fn remove(&mut self, mut ptr: NonNull) { + let p = self.queue.iter_mut_from_raw(ptr.as_mut().link.raw()).remove().unwrap(); + assert_eq!(p, ptr); + ptr.as_mut().base_mut().set_in_eviction(false); + } + + unsafe fn clear(&mut self) -> Vec> { + let mut res = Vec::with_capacity(self.len()); + while let Some(mut ptr) = self.queue.pop_front() { + ptr.as_mut().base_mut().set_in_eviction(false); + res.push(ptr); + } + res + } + + unsafe fn len(&self) -> usize { + self.queue.len() + } + + unsafe fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +unsafe impl Send for Fifo where T: Send + Sync + 'static {} +unsafe impl Sync for Fifo where T: Send + Sync + 'static {} + +#[cfg(test)] +pub mod tests { + + use itertools::Itertools; + + use super::*; + use crate::eviction::test_utils::TestEviction; + + impl TestEviction for Fifo + where + T: Send + Sync + 'static + Clone, + { + fn dump(&self) -> Vec { + self.queue + .iter() + .map(|handle| handle.base().data_unwrap_unchecked().clone()) + .collect_vec() + } + } + + type TestFifoHandle = FifoHandle; + type TestFifo = Fifo; + + unsafe fn new_test_fifo_handle_ptr(data: u64) -> NonNull { + let mut handle = Box::new(TestFifoHandle::new()); + handle.init(0, data, 1, FifoContext); + NonNull::new_unchecked(Box::into_raw(handle)) + } + + unsafe fn del_test_fifo_handle_ptr(ptr: NonNull) { + let _ = Box::from_raw(ptr.as_ptr()); + } + + #[test] + fn test_fifo() { + unsafe { + let ptrs = (0..8).map(|i| new_test_fifo_handle_ptr(i)).collect_vec(); + + let mut fifo = TestFifo::new(100, &FifoConfig {}); + + // 0, 1, 2, 3 + fifo.push(ptrs[0]); + fifo.push(ptrs[1]); + fifo.push(ptrs[2]); + fifo.push(ptrs[3]); + + // 2, 3 + let p0 = fifo.pop().unwrap(); + let p1 = fifo.pop().unwrap(); + assert_eq!(ptrs[0], p0); + assert_eq!(ptrs[1], p1); + + // 2, 3, 4, 5, 6 + fifo.push(ptrs[4]); + fifo.push(ptrs[5]); + fifo.push(ptrs[6]); + + // 2, 6 + fifo.remove(ptrs[3]); + fifo.remove(ptrs[4]); + fifo.remove(ptrs[5]); + + assert_eq!(fifo.clear(), vec![ptrs[2], ptrs[6]]); + + for ptr in ptrs { + del_test_fifo_handle_ptr(ptr); + } + } + } +} diff --git a/foyer-memory/src/eviction/lfu.rs b/foyer-memory/src/eviction/lfu.rs new file mode 100644 index 00000000..f1837562 --- /dev/null +++ b/foyer-memory/src/eviction/lfu.rs @@ -0,0 +1,541 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, ptr::NonNull}; + +use cmsketch::CMSketchU16; +use foyer_intrusive::{ + collections::dlist::{Dlist, DlistLink}, + core::adapter::Link, + intrusive_adapter, +}; + +use crate::{ + eviction::Eviction, + handle::{BaseHandle, Handle}, + CacheContext, +}; + +#[derive(Debug, Clone)] +pub struct LfuConfig { + /// `window` capacity ratio of the total cache capacity. + /// + /// Must be in (0, 1). + /// + /// Must guarantee `window_capacity_ratio + protected_capacity_ratio < 1`. + pub window_capacity_ratio: f64, + /// `protected` capacity ratio of the total cache capacity. + /// + /// Must be in (0, 1). + /// + /// Must guarantee `window_capacity_ratio + protected_capacity_ratio < 1`. + pub protected_capacity_ratio: f64, + + pub cmsketch_eps: f64, + pub cmsketch_confidence: f64, +} +#[derive(Debug, Clone)] +pub struct LfuContext; + +impl From for LfuContext { + fn from(_: CacheContext) -> Self { + Self + } +} + +impl From for CacheContext { + fn from(_: LfuContext) -> Self { + CacheContext::Default + } +} + +#[derive(Debug, PartialEq, Eq)] +enum Queue { + None, + Window, + Probation, + Protected, +} + +pub struct LfuHandle +where + T: Send + Sync + 'static, +{ + link: DlistLink, + base: BaseHandle, + queue: Queue, +} + +impl Debug for LfuHandle +where + T: Send + Sync + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LfuHandle").finish() + } +} + +intrusive_adapter! { LfuHandleDlistAdapter = NonNull>: LfuHandle { link: DlistLink } where T: Send + Sync + 'static } + +impl Handle for LfuHandle +where + T: Send + Sync + 'static, +{ + type Data = T; + type Context = LfuContext; + + fn new() -> Self { + Self { + link: DlistLink::default(), + base: BaseHandle::new(), + queue: Queue::None, + } + } + + fn init(&mut self, hash: u64, data: Self::Data, charge: usize, context: Self::Context) { + self.base.init(hash, data, charge, context) + } + + fn base(&self) -> &BaseHandle { + &self.base + } + + fn base_mut(&mut self) -> &mut BaseHandle { + &mut self.base + } +} + +unsafe impl Send for LfuHandle where T: Send + Sync + 'static {} +unsafe impl Sync for LfuHandle where T: Send + Sync + 'static {} + +/// This implementation is inspired by [Caffeine](https://github.com/ben-manes/caffeine) under Apache License 2.0 +/// +/// A newcoming and hot entry is kept in `window`. +/// +/// When `window` is full, entries from it will overflow to `probation`. +/// +/// When a entry in `probation` is accessed, it will be promoted to `protected`. +/// +/// When `protected` is full, entries from it will overflow to `probation`. +/// +/// When evicting, the entry with a lower frequency from `window` or `probtion` will be evicted first, then from +/// `protected`. +pub struct Lfu +where + T: Send + Sync + 'static, +{ + window: Dlist>, + probation: Dlist>, + protected: Dlist>, + + window_charges: usize, + probation_charges: usize, + protected_charges: usize, + + window_charges_capacity: usize, + protected_charges_capacity: usize, + + frequencies: CMSketchU16, + + step: usize, + decay: usize, +} + +impl Lfu +where + T: Send + Sync + 'static, +{ + fn increase_queue_charges(&mut self, handle: &LfuHandle) { + let charges = handle.base().charge(); + match handle.queue { + Queue::None => unreachable!(), + Queue::Window => self.window_charges += charges, + Queue::Probation => self.probation_charges += charges, + Queue::Protected => self.protected_charges += charges, + } + } + + fn decrease_queue_charges(&mut self, handle: &LfuHandle) { + let charges = handle.base().charge(); + match handle.queue { + Queue::None => unreachable!(), + Queue::Window => self.window_charges -= charges, + Queue::Probation => self.probation_charges -= charges, + Queue::Protected => self.protected_charges -= charges, + } + } + + fn update_frequencies(&mut self, hash: u64) { + self.frequencies.inc(hash); + self.step += 1; + if self.step >= self.decay { + self.step >>= 1; + self.frequencies.halve(); + } + } +} + +impl Eviction for Lfu +where + T: Send + Sync + 'static, +{ + type Item = LfuHandle; + type Config = LfuConfig; + + unsafe fn new(capacity: usize, config: &Self::Config) -> Self + where + Self: Sized, + { + assert!( + config.window_capacity_ratio > 0.0 && config.window_capacity_ratio < 1.0, + "window_capacity_ratio must be in (0, 1), given: {}", + config.window_capacity_ratio + ); + + assert!( + config.protected_capacity_ratio > 0.0 && config.protected_capacity_ratio < 1.0, + "protected_capacity_ratio must be in (0, 1), given: {}", + config.protected_capacity_ratio + ); + + assert!( + config.window_capacity_ratio + config.protected_capacity_ratio < 1.0, + "must guarantee: window_capacity_ratio + protected_capacity_ratio < 1, given: {}", + config.window_capacity_ratio + config.protected_capacity_ratio + ); + + let window_charges_capacity = (capacity as f64 * config.window_capacity_ratio) as usize; + let protected_charges_capacity = (capacity as f64 * config.protected_capacity_ratio) as usize; + let frequencies = CMSketchU16::new(config.cmsketch_eps, config.cmsketch_confidence); + let decay = frequencies.width(); + + Self { + window: Dlist::new(), + probation: Dlist::new(), + protected: Dlist::new(), + window_charges: 0, + probation_charges: 0, + protected_charges: 0, + window_charges_capacity, + protected_charges_capacity, + frequencies, + step: 0, + decay, + } + } + + unsafe fn push(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + debug_assert!(!handle.link.is_linked()); + debug_assert!(!handle.base().is_in_eviction()); + debug_assert_eq!(handle.queue, Queue::None); + + self.window.push_back(ptr); + handle.base_mut().set_in_eviction(true); + handle.queue = Queue::Window; + + self.increase_queue_charges(handle); + self.update_frequencies(handle.base().hash()); + + // If `window` charges exceeds the capacity, overflow entry from `window` to `probation`. + while self.window_charges > self.window_charges_capacity { + debug_assert!(!self.window.is_empty()); + let mut ptr = self.window.pop_front().unwrap_unchecked(); + let handle = ptr.as_mut(); + self.decrease_queue_charges(handle); + handle.queue = Queue::Probation; + self.increase_queue_charges(handle); + self.probation.push_back(ptr); + } + } + + unsafe fn pop(&mut self) -> Option> { + // Compare the frequency of the front element of `window` and `probation` queue, and evict the lower one. + // If both `window` and `probation` are empty, try evict from `protected`. + let mut ptr = match (self.window.front(), self.probation.front()) { + (None, None) => None, + (None, Some(_)) => self.probation.pop_front(), + (Some(_), None) => self.window.pop_front(), + (Some(window), Some(probation)) => { + if self.frequencies.estimate(window.base().hash()) < self.frequencies.estimate(probation.base().hash()) + { + self.window.pop_front() + + // TODO(MrCroxx): Rotate probation to prevent a high frequency but cold head holds back promotion + // too long like CacheLib does? + } else { + self.probation.pop_front() + } + } + } + .or_else(|| self.protected.pop_front())?; + + let handle = ptr.as_mut(); + + debug_assert!(!handle.link.is_linked()); + debug_assert!(handle.base().is_in_eviction()); + debug_assert_ne!(handle.queue, Queue::None); + + self.decrease_queue_charges(handle); + handle.queue = Queue::None; + handle.base_mut().set_in_eviction(false); + + Some(ptr) + } + + unsafe fn reinsert(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + match handle.queue { + Queue::None => { + debug_assert!(!handle.link.is_linked()); + debug_assert!(!handle.base().is_in_eviction()); + self.push(ptr); + debug_assert!(handle.link.is_linked()); + debug_assert!(handle.base().is_in_eviction()); + } + Queue::Window => { + // Move to MRU position of `window`. + debug_assert!(handle.link.is_linked()); + debug_assert!(handle.base().is_in_eviction()); + self.window.remove_raw(handle.link.raw()); + self.window.push_back(ptr); + } + Queue::Probation => { + // Promote to MRU position of `protected`. + debug_assert!(handle.link.is_linked()); + debug_assert!(handle.base().is_in_eviction()); + self.probation.remove_raw(handle.link.raw()); + self.decrease_queue_charges(handle); + handle.queue = Queue::Protected; + self.increase_queue_charges(handle); + self.protected.push_back(ptr); + + // If `protected` charges exceeds the capacity, overflow entry from `protected` to `probation`. + while self.protected_charges > self.protected_charges_capacity { + debug_assert!(!self.protected.is_empty()); + let mut ptr = self.protected.pop_front().unwrap_unchecked(); + let handle = ptr.as_mut(); + self.decrease_queue_charges(handle); + handle.queue = Queue::Probation; + self.increase_queue_charges(handle); + self.probation.push_back(ptr); + } + } + Queue::Protected => { + // Move to MRU position of `protected`. + debug_assert!(handle.link.is_linked()); + debug_assert!(handle.base().is_in_eviction()); + self.protected.remove_raw(handle.link.raw()); + self.protected.push_back(ptr); + } + } + } + + unsafe fn access(&mut self, ptr: NonNull) { + self.update_frequencies(ptr.as_ref().base().hash()); + } + + unsafe fn remove(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + debug_assert!(handle.link.is_linked()); + debug_assert!(handle.base().is_in_eviction()); + debug_assert_ne!(handle.queue, Queue::None); + + match handle.queue { + Queue::None => unreachable!(), + Queue::Window => self.window.remove_raw(handle.link.raw()), + Queue::Probation => self.probation.remove_raw(handle.link.raw()), + Queue::Protected => self.protected.remove_raw(handle.link.raw()), + }; + + debug_assert!(!handle.link.is_linked()); + + self.decrease_queue_charges(handle); + handle.queue = Queue::None; + handle.base_mut().set_in_eviction(false); + } + + unsafe fn clear(&mut self) -> Vec> { + let mut res = Vec::with_capacity(self.len()); + + while !self.is_empty() { + let ptr = self.pop().unwrap_unchecked(); + debug_assert!(!ptr.as_ref().base().is_in_eviction()); + debug_assert!(!ptr.as_ref().link.is_linked()); + debug_assert_eq!(ptr.as_ref().queue, Queue::None); + res.push(ptr); + } + + res + } + + unsafe fn len(&self) -> usize { + self.window.len() + self.probation.len() + self.protected.len() + } + + unsafe fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +unsafe impl Send for Lfu where T: Send + Sync + 'static {} +unsafe impl Sync for Lfu where T: Send + Sync + 'static {} + +#[cfg(test)] +mod tests { + + use itertools::Itertools; + + use super::*; + use crate::eviction::test_utils::TestEviction; + + impl TestEviction for Lfu + where + T: Send + Sync + 'static + Clone, + { + fn dump(&self) -> Vec { + self.window + .iter() + .chain(self.probation.iter()) + .chain(self.protected.iter()) + .map(|handle| handle.base().data_unwrap_unchecked().clone()) + .collect_vec() + } + } + + type TestLfu = Lfu; + type TestLfuHandle = LfuHandle; + + unsafe fn assert_test_lfu( + lfu: &TestLfu, + len: usize, + window: usize, + probation: usize, + protected: usize, + entries: Vec, + ) { + assert_eq!(lfu.len(), len); + assert_eq!(lfu.window.len(), window); + assert_eq!(lfu.probation.len(), probation); + assert_eq!(lfu.protected.len(), protected); + assert_eq!(lfu.window_charges, window); + assert_eq!(lfu.probation_charges, probation); + assert_eq!(lfu.protected_charges, protected); + let es = lfu.dump().into_iter().collect_vec(); + assert_eq!(es, entries); + } + + fn assert_min_frequency(lfu: &TestLfu, hash: u64, count: usize) { + let freq = lfu.frequencies.estimate(hash); + assert!(freq >= count as u16, "assert {freq} >= {count} failed for {hash}"); + } + + #[test] + fn test_lfu() { + unsafe { + let ptrs = (0..100) + .map(|i| { + let mut handle = Box::new(TestLfuHandle::new()); + handle.init(i, i, 1, LfuContext); + NonNull::new_unchecked(Box::into_raw(handle)) + }) + .collect_vec(); + + // window: 2, probation: 2, protected: 6 + let config = LfuConfig { + window_capacity_ratio: 0.2, + protected_capacity_ratio: 0.6, + cmsketch_eps: 0.01, + cmsketch_confidence: 0.95, + }; + let mut lfu = TestLfu::new(10, &config); + + assert_eq!(lfu.window_charges_capacity, 2); + assert_eq!(lfu.protected_charges_capacity, 6); + + lfu.push(ptrs[0]); + lfu.push(ptrs[1]); + assert_test_lfu(&lfu, 2, 2, 0, 0, vec![0, 1]); + + lfu.push(ptrs[2]); + lfu.push(ptrs[3]); + assert_test_lfu(&lfu, 4, 2, 2, 0, vec![2, 3, 0, 1]); + + (4..10).for_each(|i| lfu.push(ptrs[i])); + assert_test_lfu(&lfu, 10, 2, 8, 0, vec![8, 9, 0, 1, 2, 3, 4, 5, 6, 7]); + + (0..10).for_each(|i| assert_min_frequency(&lfu, i, 1)); + + // [8, 9] [1, 2, 3, 4, 5, 6, 7] + let p0 = lfu.pop().unwrap(); + assert_eq!(p0, ptrs[0]); + + // [9, 0] [1, 2, 3, 4, 5, 6, 7, 8] + lfu.reinsert(p0); + assert_test_lfu(&lfu, 10, 2, 8, 0, vec![9, 0, 1, 2, 3, 4, 5, 6, 7, 8]); + + // [0, 9] [1, 2, 3, 4, 5, 6, 7, 8] + lfu.reinsert(ptrs[9]); + assert_test_lfu(&lfu, 10, 2, 8, 0, vec![0, 9, 1, 2, 3, 4, 5, 6, 7, 8]); + + // [0, 9] [1, 2, 7, 8] [3, 4, 5, 6] + (3..7).for_each(|i| lfu.reinsert(ptrs[i])); + assert_test_lfu(&lfu, 10, 2, 4, 4, vec![0, 9, 1, 2, 7, 8, 3, 4, 5, 6]); + + // [0, 9] [1, 2, 7, 8] [5, 6, 3, 4] + (3..5).for_each(|i| lfu.reinsert(ptrs[i])); + assert_test_lfu(&lfu, 10, 2, 4, 4, vec![0, 9, 1, 2, 7, 8, 5, 6, 3, 4]); + + // [0, 9] [5, 6] [3, 4, 1, 2, 7, 8] + [1, 2, 7, 8].into_iter().for_each(|i| lfu.reinsert(ptrs[i])); + assert_test_lfu(&lfu, 10, 2, 2, 6, vec![0, 9, 5, 6, 3, 4, 1, 2, 7, 8]); + + // [0, 9] [6] [3, 4, 1, 2, 7, 8] + let p5 = lfu.pop().unwrap(); + assert_eq!(p5, ptrs[5]); + assert_test_lfu(&lfu, 9, 2, 1, 6, vec![0, 9, 6, 3, 4, 1, 2, 7, 8]); + + (10..13).for_each(|i| lfu.push(ptrs[i])); + + // [11, 12] [6, 0, 9, 10] [3, 4, 1, 2, 7, 8] + assert_test_lfu(&lfu, 12, 2, 4, 6, vec![11, 12, 6, 0, 9, 10, 3, 4, 1, 2, 7, 8]); + (1..13).for_each(|i| assert_min_frequency(&lfu, i, 0)); + lfu.access(ptrs[0]); + assert_min_frequency(&lfu, 0, 2); + + // evict 11 because freq(11) < freq(0) + // [12] [0, 9, 10] [3, 4, 1, 2, 7, 8] + let p6 = lfu.pop().unwrap(); + let p11 = lfu.pop().unwrap(); + assert_eq!(p6, ptrs[6]); + assert_eq!(p11, ptrs[11]); + assert_test_lfu(&lfu, 10, 1, 3, 6, vec![12, 0, 9, 10, 3, 4, 1, 2, 7, 8]); + + assert_eq!( + lfu.clear(), + [12, 0, 9, 10, 3, 4, 1, 2, 7, 8] + .into_iter() + .map(|i| ptrs[i]) + .collect_vec() + ); + + for ptr in ptrs { + let _ = Box::from_raw(ptr.as_ptr()); + } + } + } +} diff --git a/foyer-memory/src/eviction/lru.rs b/foyer-memory/src/eviction/lru.rs new file mode 100644 index 00000000..0e18a5d1 --- /dev/null +++ b/foyer-memory/src/eviction/lru.rs @@ -0,0 +1,435 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, ptr::NonNull}; + +use foyer_intrusive::{ + collections::dlist::{Dlist, DlistLink}, + core::adapter::Link, + intrusive_adapter, +}; + +use crate::{ + eviction::Eviction, + handle::{BaseHandle, Handle}, + CacheContext, +}; + +#[derive(Debug, Clone)] +pub struct LruConfig { + /// The ratio of the high priority pool occupied. + /// + /// [`Lru`] guarantees that the high priority charges are always as larger as + /// but no larger that the capacity * high priority pool ratio. + /// + /// # Panic + /// + /// Panics if the value is not in [0, 1.0]. + pub high_priority_pool_ratio: f64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LruContext { + HighPriority, + LowPriority, +} + +impl From for LruContext { + fn from(value: CacheContext) -> Self { + match value { + CacheContext::Default => Self::HighPriority, + CacheContext::LruPriorityLow => Self::LowPriority, + } + } +} + +impl From for CacheContext { + fn from(value: LruContext) -> Self { + match value { + LruContext::HighPriority => CacheContext::Default, + LruContext::LowPriority => CacheContext::LruPriorityLow, + } + } +} + +pub struct LruHandle +where + T: Send + Sync + 'static, +{ + link: DlistLink, + base: BaseHandle, + in_high_priority_pool: bool, +} + +impl Debug for LruHandle +where + T: Send + Sync + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LruHandle").finish() + } +} + +intrusive_adapter! { LruHandleDlistAdapter = NonNull>: LruHandle { link: DlistLink } where T: Send + Sync + 'static } + +impl Handle for LruHandle +where + T: Send + Sync + 'static, +{ + type Data = T; + type Context = LruContext; + + fn new() -> Self { + Self { + link: DlistLink::default(), + base: BaseHandle::new(), + in_high_priority_pool: false, + } + } + + fn init(&mut self, hash: u64, data: Self::Data, charge: usize, context: Self::Context) { + self.base.init(hash, data, charge, context) + } + + fn base(&self) -> &BaseHandle { + &self.base + } + + fn base_mut(&mut self) -> &mut BaseHandle { + &mut self.base + } +} + +unsafe impl Send for LruHandle where T: Send + Sync + 'static {} +unsafe impl Sync for LruHandle where T: Send + Sync + 'static {} + +pub struct Lru +where + T: Send + Sync + 'static, +{ + high_priority_list: Dlist>, + list: Dlist>, + + high_priority_charges: usize, + high_priority_charges_capacity: usize, +} + +impl Lru +where + T: Send + Sync + 'static, +{ + unsafe fn may_overflow_high_priority_pool(&mut self) { + while self.high_priority_charges > self.high_priority_charges_capacity { + debug_assert!(!self.high_priority_list.is_empty()); + + // overflow last entry in high priority pool to low priority pool + let mut ptr = self.high_priority_list.pop_front().unwrap_unchecked(); + ptr.as_mut().in_high_priority_pool = false; + self.high_priority_charges -= ptr.as_ref().base().charge(); + self.list.push_back(ptr); + } + } +} + +impl Eviction for Lru +where + T: Send + Sync + 'static, +{ + type Item = LruHandle; + type Config = LruConfig; + + unsafe fn new(capacity: usize, config: &Self::Config) -> Self + where + Self: Sized, + { + assert!( + config.high_priority_pool_ratio >= 0.0 && config.high_priority_pool_ratio <= 1.0, + "high_priority_pool_ratio_percentage must be in [0, 100], given: {}", + config.high_priority_pool_ratio + ); + + let high_priority_charges_capacity = (capacity as f64 * config.high_priority_pool_ratio) as usize; + + Self { + high_priority_list: Dlist::new(), + list: Dlist::new(), + high_priority_charges: 0, + high_priority_charges_capacity, + } + } + + unsafe fn push(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + debug_assert!(!handle.link.is_linked()); + + match handle.base().context() { + LruContext::HighPriority => { + handle.in_high_priority_pool = true; + self.high_priority_charges += handle.base().charge(); + self.high_priority_list.push_back(ptr); + + self.may_overflow_high_priority_pool(); + } + LruContext::LowPriority => { + handle.in_high_priority_pool = false; + self.list.push_back(ptr); + } + } + + handle.base_mut().set_in_eviction(true); + } + + unsafe fn pop(&mut self) -> Option> { + let mut ptr = self.list.pop_front().or_else(|| self.high_priority_list.pop_front())?; + + let handle = ptr.as_mut(); + debug_assert!(!handle.link.is_linked()); + + if handle.in_high_priority_pool { + self.high_priority_charges -= handle.base().charge(); + } + + handle.base_mut().set_in_eviction(false); + + Some(ptr) + } + + unsafe fn access(&mut self, _: NonNull) {} + + unsafe fn reinsert(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + if handle.base().is_in_eviction() { + debug_assert!(handle.link.is_linked()); + self.remove(ptr); + self.push(ptr); + } else { + debug_assert!(!handle.link.is_linked()); + self.push(ptr); + } + } + + unsafe fn remove(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + debug_assert!(handle.link.is_linked()); + + if handle.in_high_priority_pool { + self.high_priority_charges -= handle.base.charge(); + self.high_priority_list.remove_raw(handle.link.raw()); + } else { + self.list.remove_raw(handle.link.raw()); + } + + handle.base_mut().set_in_eviction(false); + } + + unsafe fn clear(&mut self) -> Vec> { + let mut res = Vec::with_capacity(self.len()); + + while !self.list.is_empty() { + let mut ptr = self.list.pop_front().unwrap_unchecked(); + ptr.as_mut().base_mut().set_in_eviction(false); + res.push(ptr); + } + + while !self.high_priority_list.is_empty() { + let mut ptr = self.high_priority_list.pop_front().unwrap_unchecked(); + ptr.as_mut().base_mut().set_in_eviction(false); + self.high_priority_charges -= ptr.as_ref().base().charge(); + res.push(ptr); + } + + debug_assert_eq!(self.high_priority_charges, 0); + + res + } + + unsafe fn len(&self) -> usize { + self.high_priority_list.len() + self.list.len() + } + + unsafe fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +unsafe impl Send for Lru where T: Send + Sync + 'static {} +unsafe impl Sync for Lru where T: Send + Sync + 'static {} + +#[cfg(test)] +pub mod tests { + + use foyer_intrusive::core::pointer::Pointer; + use itertools::Itertools; + + use super::*; + use crate::eviction::test_utils::TestEviction; + + impl TestEviction for Lru + where + T: Send + Sync + 'static + Clone, + { + fn dump(&self) -> Vec { + self.list + .iter() + .chain(self.high_priority_list.iter()) + .map(|handle| handle.base().data_unwrap_unchecked().clone()) + .collect_vec() + } + } + + type TestLruHandle = LruHandle; + type TestLru = Lru; + + unsafe fn new_test_lru_handle_ptr(data: u64, context: LruContext) -> NonNull { + let mut handle = Box::new(TestLruHandle::new()); + handle.init(0, data, 1, context); + NonNull::new_unchecked(Box::into_raw(handle)) + } + + unsafe fn del_test_lru_handle_ptr(ptr: NonNull) { + let _ = Box::from_raw(ptr.as_ptr()); + } + + unsafe fn dump_test_lru(lru: &TestLru) -> (Vec>, Vec>) { + ( + lru.list + .iter() + .map(|handle| NonNull::new_unchecked(handle.as_ptr() as *mut _)) + .collect_vec(), + lru.high_priority_list + .iter() + .map(|handle| NonNull::new_unchecked(handle.as_ptr() as *mut _)) + .collect_vec(), + ) + } + + #[test] + fn test_lru() { + unsafe { + let ptrs = (0..20) + .map(|i| { + new_test_lru_handle_ptr( + i, + if i < 10 { + LruContext::HighPriority + } else { + LruContext::LowPriority + }, + ) + }) + .collect_vec(); + + let config = LruConfig { + high_priority_pool_ratio: 0.5, + }; + let mut lru = TestLru::new(8, &config); + + assert_eq!(lru.high_priority_charges_capacity, 4); + + // [0, 1, 2, 3] + lru.push(ptrs[0]); + lru.push(ptrs[1]); + lru.push(ptrs[2]); + lru.push(ptrs[3]); + assert_eq!(lru.len(), 4); + assert_eq!(lru.high_priority_charges, 4); + assert_eq!(lru.high_priority_list.len(), 4); + assert_eq!(dump_test_lru(&lru), (vec![], vec![ptrs[0], ptrs[1], ptrs[2], ptrs[3]])); + + // 0, [1, 2, 3, 4] + lru.push(ptrs[4]); + assert_eq!(lru.len(), 5); + assert_eq!(lru.high_priority_charges, 4); + assert_eq!(lru.high_priority_list.len(), 4); + assert_eq!( + dump_test_lru(&lru), + (vec![ptrs[0]], vec![ptrs[1], ptrs[2], ptrs[3], ptrs[4]]) + ); + + // 0, 10, [1, 2, 3, 4] + lru.push(ptrs[10]); + assert_eq!(lru.len(), 6); + assert_eq!(lru.high_priority_charges, 4); + assert_eq!(lru.high_priority_list.len(), 4); + assert_eq!( + dump_test_lru(&lru), + (vec![ptrs[0], ptrs[10]], vec![ptrs[1], ptrs[2], ptrs[3], ptrs[4]]) + ); + + // 10, [1, 2, 3, 4] + let p0 = lru.pop().unwrap(); + assert_eq!(ptrs[0], p0); + assert_eq!(lru.len(), 5); + assert_eq!(lru.high_priority_charges, 4); + assert_eq!(lru.high_priority_list.len(), 4); + assert_eq!( + dump_test_lru(&lru), + (vec![ptrs[10]], vec![ptrs[1], ptrs[2], ptrs[3], ptrs[4]]) + ); + + // 10, [1, 3, 4] + lru.remove(ptrs[2]); + assert_eq!(lru.len(), 4); + assert_eq!(lru.high_priority_charges, 3); + assert_eq!(lru.high_priority_list.len(), 3); + assert_eq!(dump_test_lru(&lru), (vec![ptrs[10]], vec![ptrs[1], ptrs[3], ptrs[4]])); + + // 10, 11, [1, 3, 4] + lru.push(ptrs[11]); + assert_eq!(lru.len(), 5); + assert_eq!(lru.high_priority_charges, 3); + assert_eq!(lru.high_priority_list.len(), 3); + assert_eq!( + dump_test_lru(&lru), + (vec![ptrs[10], ptrs[11]], vec![ptrs[1], ptrs[3], ptrs[4]]) + ); + + // 10, 11, 1, [3, 4, 5, 6] + lru.push(ptrs[5]); + lru.push(ptrs[6]); + assert_eq!(lru.len(), 7); + assert_eq!(lru.high_priority_charges, 4); + assert_eq!(lru.high_priority_list.len(), 4); + assert_eq!( + dump_test_lru(&lru), + ( + vec![ptrs[10], ptrs[11], ptrs[1]], + vec![ptrs[3], ptrs[4], ptrs[5], ptrs[6]] + ) + ); + + // 10, 11, 1, 3, [4, 5, 6, 0] + lru.push(ptrs[0]); + assert_eq!(lru.len(), 8); + assert_eq!(lru.high_priority_charges, 4); + assert_eq!(lru.high_priority_list.len(), 4); + assert_eq!( + dump_test_lru(&lru), + ( + vec![ptrs[10], ptrs[11], ptrs[1], ptrs[3]], + vec![ptrs[4], ptrs[5], ptrs[6], ptrs[0]] + ) + ); + + let ps = lru.clear(); + assert_eq!(ps, [10, 11, 1, 3, 4, 5, 6, 0].map(|i| ptrs[i])); + + for ptr in ptrs { + del_test_lru_handle_ptr(ptr); + } + } + } +} diff --git a/foyer-memory/src/eviction/mod.rs b/foyer-memory/src/eviction/mod.rs new file mode 100644 index 00000000..4b9d4b09 --- /dev/null +++ b/foyer-memory/src/eviction/mod.rs @@ -0,0 +1,108 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ptr::NonNull; + +/// The lifetime of `handle: Self::H` is managed by [`Indexer`]. +/// +/// Each `handle`'s lifetime in [`Indexer`] must outlive the raw pointer in [`Eviction`]. +pub trait Eviction: Send + Sync + 'static { + type Item; + type Config; + + /// Create a new empty eviction container. + /// + /// # Safety + unsafe fn new(capacity: usize, config: &Self::Config) -> Self + where + Self: Sized; + + /// Push a handle `ptr` into the eviction container. + /// + /// The caller guarantees that the `ptr` is NOT in the eviction container. + /// + /// # Safety + /// + /// The `ptr` must be kept holding until `pop` or `remove`. + /// + /// The base handle associated to the `ptr` must be set in cache. + unsafe fn push(&mut self, ptr: NonNull); + + /// Pop a handle `ptr` from the eviction container. + /// + /// # Safety + /// + /// The `ptr` must be taken from the eviction container. + /// Or it may become dangling and cause UB. + /// + /// The base handle associated to the `ptr` must be set NOT in cache. + unsafe fn pop(&mut self) -> Option>; + + /// Try to reinsert a handle `ptr` into the eviction container after access. + /// + /// The eviction container can decide if to insert based on the algorithm. + /// + /// # Safety + /// + /// The given `ptr` may be either IN or NOT IN the eviction container. + /// If the `ptr` is reinserted, the base handle associated to it must be set in cache. + /// If the `ptr` is NOT reinserted, the base handle associated to it must be set NOT in cache. + unsafe fn reinsert(&mut self, ptr: NonNull); + + /// Notify the eviciton container that the `ptr` is accessed. + /// The eviction container can update its statistics. + /// + /// # Safety + /// + /// The given `ptr` can be EITHER in the eviction container OR not in the eviction container. + unsafe fn access(&mut self, ptr: NonNull); + + /// Remove the given `ptr` from the eviction container. + /// + /// /// The caller guarantees that the `ptr` is NOT in the eviction container. + /// + /// # Safety + /// + /// The `ptr` must be taken from the eviction container, otherwise it may become dangling and cause UB. + /// + /// The base handle associated to the `ptr` must be set NOT in cache. + unsafe fn remove(&mut self, ptr: NonNull); + + /// Remove all `ptr`s from the eviction container and reset. + /// + /// # Safety + /// + /// All `ptr` must be taken from the eviction container, otherwise it may become dangling and cause UB. + /// + /// All base handles associated to the `ptr`s must be set NOT in cache. + unsafe fn clear(&mut self) -> Vec>; + + /// Return the count of the `ptr`s that in the eviction container. + /// + /// # Safety + unsafe fn len(&self) -> usize; + + /// Return `true` if the eviction container is empty. + /// + /// # Safety + unsafe fn is_empty(&self) -> bool; +} + +pub mod fifo; +pub mod lfu; +pub mod lru; +pub mod s3fifo; + +#[cfg(test)] +pub mod test_utils; diff --git a/foyer-memory/src/eviction/s3fifo.rs b/foyer-memory/src/eviction/s3fifo.rs new file mode 100644 index 00000000..d21a50bc --- /dev/null +++ b/foyer-memory/src/eviction/s3fifo.rs @@ -0,0 +1,394 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, ptr::NonNull}; + +use foyer_intrusive::{ + collections::dlist::{Dlist, DlistLink}, + intrusive_adapter, +}; + +use crate::{ + eviction::Eviction, + handle::{BaseHandle, Handle}, + CacheContext, +}; + +#[derive(Debug, Clone)] +pub struct S3FifoContext; + +impl From for S3FifoContext { + fn from(_: CacheContext) -> Self { + Self + } +} + +impl From for CacheContext { + fn from(_: S3FifoContext) -> Self { + CacheContext::Default + } +} + +enum Queue { + None, + Main, + Small, +} + +pub struct S3FifoHandle +where + T: Send + Sync + 'static, +{ + link: DlistLink, + base: BaseHandle, + freq: u8, + queue: Queue, +} + +impl Debug for S3FifoHandle +where + T: Send + Sync + 'static, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("S3FifoHandle").finish() + } +} + +intrusive_adapter! { S3FifoHandleDlistAdapter = NonNull>: S3FifoHandle { link: DlistLink } where T: Send + Sync + 'static } + +impl S3FifoHandle +where + T: Send + Sync + 'static, +{ + #[inline(always)] + pub fn inc(&mut self) { + self.freq = std::cmp::min(self.freq + 1, 3); + } + + #[inline(always)] + pub fn dec(&mut self) { + self.freq = self.freq.saturating_sub(1); + } + + #[inline(always)] + pub fn reset(&mut self) { + self.freq = 0; + } +} + +impl Handle for S3FifoHandle +where + T: Send + Sync + 'static, +{ + type Data = T; + type Context = S3FifoContext; + + fn new() -> Self { + Self { + link: DlistLink::default(), + freq: 0, + base: BaseHandle::new(), + queue: Queue::None, + } + } + + fn init(&mut self, hash: u64, data: Self::Data, charge: usize, context: Self::Context) { + self.base.init(hash, data, charge, context); + } + + fn base(&self) -> &BaseHandle { + &self.base + } + + fn base_mut(&mut self) -> &mut BaseHandle { + &mut self.base + } +} + +#[derive(Debug, Clone)] +pub struct S3FifoConfig { + pub small_queue_capacity_ratio: f64, +} + +pub struct S3Fifo +where + T: Send + Sync + 'static, +{ + small_queue: Dlist>, + main_queue: Dlist>, + + small_capacity: usize, + + small_charges: usize, + main_charges: usize, +} + +impl S3Fifo +where + T: Send + Sync + 'static, +{ + unsafe fn evict(&mut self) -> Option>> { + // TODO(MrCroxx): Use `let_chains` here after it is stable. + if self.small_charges > self.small_capacity { + if let Some(ptr) = self.evict_small() { + return Some(ptr); + } + } + self.evict_main() + } + + unsafe fn evict_small(&mut self) -> Option>> { + while let Some(mut ptr) = self.small_queue.pop_front() { + let handle = ptr.as_mut(); + if handle.freq > 1 { + self.main_queue.push_back(ptr); + handle.queue = Queue::Main; + self.small_charges -= handle.base().charge(); + self.main_charges += handle.base().charge(); + } else { + handle.queue = Queue::None; + handle.reset(); + self.small_charges -= handle.base().charge(); + return Some(ptr); + } + } + None + } + + unsafe fn evict_main(&mut self) -> Option>> { + while let Some(mut ptr) = self.main_queue.pop_front() { + let handle = ptr.as_mut(); + if handle.freq > 0 { + self.main_queue.push_back(ptr); + handle.dec(); + } else { + handle.queue = Queue::None; + self.main_charges -= handle.base.charge(); + return Some(ptr); + } + } + None + } +} + +impl Eviction for S3Fifo +where + T: Send + Sync + 'static, +{ + type Item = S3FifoHandle; + type Config = S3FifoConfig; + + unsafe fn new(capacity: usize, config: &Self::Config) -> Self + where + Self: Sized, + { + let small_capacity = (capacity as f64 * config.small_queue_capacity_ratio) as usize; + Self { + small_queue: Dlist::new(), + main_queue: Dlist::new(), + small_capacity, + small_charges: 0, + main_charges: 0, + } + } + + unsafe fn push(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + self.small_queue.push_back(ptr); + handle.queue = Queue::Small; + self.small_charges += handle.base().charge(); + + handle.base_mut().set_in_eviction(true); + } + + unsafe fn pop(&mut self) -> Option> { + if let Some(mut ptr) = self.evict() { + let handle = ptr.as_mut(); + // `handle.queue` has already been set with `evict()` + handle.base_mut().set_in_eviction(false); + Some(ptr) + } else { + debug_assert!(self.is_empty()); + None + } + } + + unsafe fn reinsert(&mut self, _: NonNull) {} + + unsafe fn access(&mut self, ptr: NonNull) { + let mut ptr = ptr; + ptr.as_mut().inc(); + } + + unsafe fn remove(&mut self, mut ptr: NonNull) { + let handle = ptr.as_mut(); + + match handle.queue { + Queue::None => unreachable!(), + Queue::Main => { + let p = self + .main_queue + .iter_mut_from_raw(ptr.as_mut().link.raw()) + .remove() + .unwrap_unchecked(); + debug_assert_eq!(p, ptr); + + handle.queue = Queue::None; + handle.base_mut().set_in_eviction(false); + + self.main_charges -= handle.base().charge(); + } + Queue::Small => { + let p = self + .small_queue + .iter_mut_from_raw(ptr.as_mut().link.raw()) + .remove() + .unwrap_unchecked(); + debug_assert_eq!(p, ptr); + + handle.queue = Queue::None; + handle.base_mut().set_in_eviction(false); + + self.small_charges -= handle.base().charge(); + } + } + } + + unsafe fn clear(&mut self) -> Vec> { + let mut res = Vec::with_capacity(self.len()); + while let Some(mut ptr) = self.small_queue.pop_front() { + let handle = ptr.as_mut(); + handle.base_mut().set_in_eviction(false); + handle.queue = Queue::None; + res.push(ptr); + } + while let Some(mut ptr) = self.main_queue.pop_front() { + let handle = ptr.as_mut(); + handle.base_mut().set_in_eviction(false); + handle.queue = Queue::None; + res.push(ptr); + } + res + } + + unsafe fn len(&self) -> usize { + self.small_queue.len() + self.main_queue.len() + } + + unsafe fn is_empty(&self) -> bool { + self.small_queue.is_empty() && self.main_queue.is_empty() + } +} + +unsafe impl Send for S3Fifo where T: Send + Sync + 'static {} +unsafe impl Sync for S3Fifo where T: Send + Sync + 'static {} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use itertools::Itertools; + + use super::*; + use crate::eviction::test_utils::TestEviction; + + impl TestEviction for S3Fifo + where + T: Send + Sync + 'static + Clone, + { + fn dump(&self) -> Vec { + self.small_queue + .iter() + .chain(self.main_queue.iter()) + .map(|handle| handle.base().data_unwrap_unchecked().clone()) + .collect_vec() + } + } + + type TestS3Fifo = S3Fifo; + type TestS3FifoHandle = S3FifoHandle; + + fn assert_test_s3fifo(s3fifo: &TestS3Fifo, small: Vec, main: Vec) { + let mut s = s3fifo.dump().into_iter().collect_vec(); + assert_eq!(s.len(), s3fifo.small_queue.len() + s3fifo.main_queue.len()); + let m = s.split_off(s3fifo.small_queue.len()); + assert_eq!((&s, &m), (&small, &main)); + assert_eq!(s3fifo.small_charges, s.len()); + } + + fn assert_count(ptrs: &[NonNull], range: Range, count: u8) { + unsafe { + ptrs[range].iter().for_each(|ptr| assert_eq!(ptr.as_ref().freq, count)); + } + } + + #[test] + fn test_lfu() { + unsafe { + let ptrs = (0..100) + .map(|i| { + let mut handle = Box::new(TestS3FifoHandle::new()); + handle.init(i, i, 1, S3FifoContext); + NonNull::new_unchecked(Box::into_raw(handle)) + }) + .collect_vec(); + + // window: 2, probation: 2, protected: 6 + let config = S3FifoConfig { + small_queue_capacity_ratio: 0.25, + }; + let mut s3fifo = TestS3Fifo::new(8, &config); + + assert_eq!(s3fifo.small_capacity, 2); + + s3fifo.push(ptrs[0]); + s3fifo.push(ptrs[1]); + assert_test_s3fifo(&s3fifo, vec![0, 1], vec![]); + + s3fifo.push(ptrs[2]); + s3fifo.push(ptrs[3]); + assert_test_s3fifo(&s3fifo, vec![0, 1, 2, 3], vec![]); + + assert_count(&ptrs, 0..4, 0); + + (0..4).for_each(|i| s3fifo.access(ptrs[i])); + s3fifo.access(ptrs[1]); + s3fifo.access(ptrs[2]); + assert_count(&ptrs, 0..1, 1); + assert_count(&ptrs, 1..3, 2); + assert_count(&ptrs, 3..4, 1); + + let p0 = s3fifo.pop().unwrap(); + let p3 = s3fifo.pop().unwrap(); + assert_eq!(p0, ptrs[0]); + assert_eq!(p3, ptrs[3]); + assert_test_s3fifo(&s3fifo, vec![], vec![1, 2]); + assert_count(&ptrs, 0..1, 0); + assert_count(&ptrs, 1..3, 2); + assert_count(&ptrs, 3..4, 0); + + let p1 = s3fifo.pop().unwrap(); + assert_eq!(p1, ptrs[1]); + assert_test_s3fifo(&s3fifo, vec![], vec![2]); + assert_count(&ptrs, 0..4, 0); + + assert_eq!(s3fifo.clear(), [2].into_iter().map(|i| ptrs[i]).collect_vec()); + + for ptr in ptrs { + let _ = Box::from_raw(ptr.as_ptr()); + } + } + } +} diff --git a/foyer-memory/src/eviction/test_utils.rs b/foyer-memory/src/eviction/test_utils.rs new file mode 100644 index 00000000..983ae8b9 --- /dev/null +++ b/foyer-memory/src/eviction/test_utils.rs @@ -0,0 +1,25 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::Eviction; +use crate::handle::Handle; + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[allow(clippy::type_complexity)] +pub trait TestEviction: Eviction +where + Self::Item: Handle, +{ + fn dump(&self) -> Vec<::Data>; +} diff --git a/foyer-memory/src/generic.rs b/foyer-memory/src/generic.rs new file mode 100644 index 00000000..9414f978 --- /dev/null +++ b/foyer-memory/src/generic.rs @@ -0,0 +1,1012 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + borrow::Borrow, + future::Future, + hash::BuildHasher, + hash::Hash, + ops::Deref, + ptr::NonNull, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +use ahash::RandomState; +use crossbeam::queue::ArrayQueue; +use futures::FutureExt; +use hashbrown::hash_map::{Entry as HashMapEntry, HashMap}; +use itertools::Itertools; +use parking_lot::Mutex; +use tokio::{sync::oneshot, task::JoinHandle}; + +use crate::{ + eviction::Eviction, + handle::{Handle, KeyedHandle}, + indexer::Indexer, + listener::CacheEventListener, + metrics::Metrics, + CacheContext, Key, Value, +}; + +struct CacheSharedState { + metrics: Metrics, + /// The object pool to avoid frequent handle allocating, shared by all shards. + object_pool: ArrayQueue>, + listener: L, +} + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[allow(clippy::type_complexity)] +struct CacheShard +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + indexer: I, + eviction: E, + + capacity: usize, + usage: Arc, + + waiters: HashMap>>>, + + state: Arc>, +} + +impl CacheShard +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn new( + capacity: usize, + eviction_config: &E::Config, + usage: Arc, + context: Arc>, + ) -> Self { + let indexer = I::new(); + let eviction = unsafe { E::new(capacity, eviction_config) }; + let waiters = HashMap::default(); + Self { + indexer, + eviction, + capacity, + usage, + waiters, + state: context, + } + } + + /// Insert a new entry into the cache. The handle for the new entry is returned. + unsafe fn insert( + &mut self, + hash: u64, + key: K, + value: V, + charge: usize, + context: ::Context, + last_reference_entries: &mut Vec<(K, V, ::Context, usize)>, + ) -> NonNull { + let mut handle = self.state.object_pool.pop().unwrap_or_else(|| Box::new(E::Item::new())); + handle.init(hash, (key, value), charge, context); + let mut ptr = unsafe { NonNull::new_unchecked(Box::into_raw(handle)) }; + + self.evict(charge, last_reference_entries); + + debug_assert!(!ptr.as_ref().base().is_in_indexer()); + if let Some(old) = self.indexer.insert(ptr) { + self.state.metrics.replace.fetch_add(1, Ordering::Relaxed); + + debug_assert!(!old.as_ref().base().is_in_indexer()); + if old.as_ref().base().is_in_eviction() { + self.eviction.remove(old); + } + debug_assert!(!old.as_ref().base().is_in_eviction()); + // Because the `old` handle is removed from the indexer, it will not be reinserted again. + if let Some(entry) = self.try_release_handle(old, false) { + last_reference_entries.push(entry); + } + } else { + self.state.metrics.insert.fetch_add(1, Ordering::Relaxed); + } + self.eviction.push(ptr); + + debug_assert!(ptr.as_ref().base().is_in_indexer()); + debug_assert!(ptr.as_ref().base().is_in_indexer()); + + self.usage.fetch_add(charge, Ordering::Relaxed); + ptr.as_mut().base_mut().inc_refs(); + + ptr + } + + unsafe fn get(&mut self, hash: u64, key: &Q) -> Option> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let mut ptr = match self.indexer.get(hash, key) { + Some(ptr) => { + self.state.metrics.hit.fetch_add(1, Ordering::Relaxed); + ptr + } + None => { + self.state.metrics.miss.fetch_add(1, Ordering::Relaxed); + return None; + } + }; + let base = ptr.as_mut().base_mut(); + debug_assert!(base.is_in_indexer()); + + base.inc_refs(); + self.eviction.access(ptr); + + Some(ptr) + } + + unsafe fn contains(&mut self, hash: u64, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + self.indexer.get(hash, key).is_some() + } + + /// Remove a key from the cache. + /// + /// Return `Some(..)` if the handle is released, or `None` if the handle is still in use. + unsafe fn remove(&mut self, hash: u64, key: &Q) -> Option<(K, V, ::Context, usize)> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let ptr = self.indexer.remove(hash, key)?; + self.state.metrics.remove.fetch_add(1, Ordering::Relaxed); + if ptr.as_ref().base().is_in_eviction() { + self.eviction.remove(ptr); + } + debug_assert!(!ptr.as_ref().base().is_in_indexer()); + debug_assert!(!ptr.as_ref().base().is_in_eviction()); + self.try_release_handle(ptr, false) + } + + /// Clear all cache entries. + unsafe fn clear(&mut self, last_reference_entries: &mut Vec<(K, V, ::Context, usize)>) { + // TODO(MrCroxx): Avoid collecting here? + let ptrs = self.indexer.drain().collect_vec(); + let eptrs = self.eviction.clear(); + + // Assert that the handles in the indexer covers the handles in the eviction container. + if cfg!(debug_assertions) { + use std::{collections::HashSet as StdHashSet, hash::RandomState as StdRandomState}; + let ptrs: StdHashSet<_, StdRandomState> = StdHashSet::from_iter(ptrs.iter().copied()); + let eptrs: StdHashSet<_, StdRandomState> = StdHashSet::from_iter(eptrs.iter().copied()); + debug_assert!((&eptrs - &ptrs).is_empty()); + } + + self.state.metrics.remove.fetch_add(ptrs.len(), Ordering::Relaxed); + + // The handles in the indexer covers the handles in the eviction container. + // So only the handles drained from the indexer need to be released. + for ptr in ptrs { + debug_assert!(!ptr.as_ref().base().is_in_indexer()); + if let Some(entry) = self.try_release_handle(ptr, false) { + last_reference_entries.push(entry); + } + } + } + + unsafe fn evict( + &mut self, + charge: usize, + last_reference_entries: &mut Vec<(K, V, ::Context, usize)>, + ) { + // TODO(MrCroxx): Use `let_chains` here after it is stable. + while self.usage.load(Ordering::Relaxed) + charge > self.capacity { + let evicted = match self.eviction.pop() { + Some(evicted) => evicted, + None => break, + }; + self.state.metrics.evict.fetch_add(1, Ordering::Relaxed); + let base = evicted.as_ref().base(); + debug_assert!(base.is_in_indexer()); + debug_assert!(!base.is_in_eviction()); + if let Some(entry) = self.try_release_handle(evicted, false) { + last_reference_entries.push(entry); + } + } + } + + /// Release a handle used by an external user. + /// + /// Return `Some(..)` if the handle is released, or `None` if the handle is still in use. + unsafe fn try_release_external_handle( + &mut self, + mut ptr: NonNull, + ) -> Option<(K, V, ::Context, usize)> { + ptr.as_mut().base_mut().dec_refs(); + self.try_release_handle(ptr, true) + } + + /// Try release handle if there is no external reference and no reinsertion is needed. + /// + /// Return the entry if the handle is released. + /// + /// Recycle it if possible. + unsafe fn try_release_handle( + &mut self, + mut ptr: NonNull, + reinsert: bool, + ) -> Option<(K, V, ::Context, usize)> { + let handle = ptr.as_mut(); + + if handle.base().has_refs() { + return None; + } + + debug_assert!(handle.base().is_inited()); + debug_assert!(!handle.base().has_refs()); + + // If the entry is not updated or removed from the cache, try to reinsert it or remove it from the indexer and + // the eviction container. + if handle.base().is_in_indexer() { + // The usage is higher than the capacity means most handles are held externally, + // the cache shard cannot release enough charges for the new inserted entries. + // In this case, the reinsertion should be given up. + if reinsert && self.usage.load(Ordering::Relaxed) <= self.capacity { + let was_in_eviction = handle.base().is_in_eviction(); + self.eviction.reinsert(ptr); + if ptr.as_ref().base().is_in_eviction() { + if was_in_eviction { + self.state.metrics.reinsert.fetch_add(1, Ordering::Relaxed); + } + return None; + } + } + + // If the entry has not been reinserted, remove it from the indexer and the eviction container (if needed). + self.indexer.remove(handle.base().hash(), handle.key()); + if ptr.as_ref().base().is_in_eviction() { + self.eviction.remove(ptr); + } + } + + // Here the handle is neither in the indexer nor in the eviction container. + debug_assert!(!handle.base().is_in_indexer()); + debug_assert!(!handle.base().is_in_eviction()); + debug_assert!(!handle.base().has_refs()); + + self.state.metrics.release.fetch_add(1, Ordering::Relaxed); + + self.usage.fetch_sub(handle.base().charge(), Ordering::Relaxed); + let ((key, value), context, charge) = handle.base_mut().take(); + + let handle = Box::from_raw(ptr.as_ptr()); + let _ = self.state.object_pool.push(handle); + + Some((key, value, context, charge)) + } +} + +impl Drop for CacheShard +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn drop(&mut self) { + unsafe { self.clear(&mut vec![]) } + } +} + +pub struct CacheConfig +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub capacity: usize, + pub shards: usize, + pub eviction_config: E::Config, + pub object_pool_capacity: usize, + pub hash_builder: S, + pub event_listener: L, +} + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[allow(clippy::type_complexity)] +pub enum GenericEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, + ER: std::error::Error, +{ + Invalid, + Hit(GenericCacheEntry), + Wait(oneshot::Receiver>), + Miss(JoinHandle, ER>>), +} + +impl Default for GenericEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, + ER: std::error::Error, +{ + fn default() -> Self { + Self::Invalid + } +} + +impl Future for GenericEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, + ER: std::error::Error + From, +{ + type Output = std::result::Result, ER>; + + fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll { + match &mut *self { + Self::Invalid => unreachable!(), + Self::Hit(_) => std::task::Poll::Ready(Ok(match std::mem::take(&mut *self) { + GenericEntry::Hit(entry) => entry, + _ => unreachable!(), + })), + Self::Wait(waiter) => waiter.poll_unpin(cx).map_err(|err| err.into()), + Self::Miss(join_handle) => join_handle.poll_unpin(cx).map(|join_result| join_result.unwrap()), + } + } +} + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[allow(clippy::type_complexity)] +pub struct GenericCache +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + shards: Vec>>, + + capacity: usize, + usages: Vec>, + + context: Arc>, + + hash_builder: S, +} + +impl GenericCache +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn new(config: CacheConfig) -> Self { + let usages = (0..config.shards).map(|_| Arc::new(AtomicUsize::new(0))).collect_vec(); + let context = Arc::new(CacheSharedState { + metrics: Metrics::default(), + object_pool: ArrayQueue::new(config.object_pool_capacity), + listener: config.event_listener, + }); + + let shard_capacity = config.capacity / config.shards; + + let shards = usages + .iter() + .map(|usage| CacheShard::new(shard_capacity, &config.eviction_config, usage.clone(), context.clone())) + .map(Mutex::new) + .collect_vec(); + + Self { + shards, + capacity: config.capacity, + usages, + context, + hash_builder: config.hash_builder, + } + } + + pub fn insert(self: &Arc, key: K, value: V, charge: usize) -> GenericCacheEntry { + self.insert_with_context(key, value, charge, CacheContext::default()) + } + + pub fn insert_with_context( + self: &Arc, + key: K, + value: V, + charge: usize, + context: CacheContext, + ) -> GenericCacheEntry { + let hash = self.hash_builder.hash_one(&key); + + let mut to_deallocate = vec![]; + + let (entry, waiters) = unsafe { + let mut shard = self.shards[hash as usize % self.shards.len()].lock(); + let waiters = shard.waiters.remove(&key); + let mut ptr = shard.insert(hash, key, value, charge, context.into(), &mut to_deallocate); + if let Some(waiters) = waiters.as_ref() { + ptr.as_mut().base_mut().inc_refs_by(waiters.len()); + } + let entry = GenericCacheEntry { + cache: self.clone(), + ptr, + }; + (entry, waiters) + }; + + if let Some(waiters) = waiters { + for waiter in waiters { + let _ = waiter.send(GenericCacheEntry { + cache: self.clone(), + ptr: entry.ptr, + }); + } + } + + // Do not deallocate data within the lock section. + for (key, value, context, charges) in to_deallocate { + self.context.listener.on_release(key, value, context.into(), charges) + } + + entry + } + + pub fn remove(&self, key: &Q) + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let hash = self.hash_builder.hash_one(key); + + let entry = unsafe { + let mut shard = self.shards[hash as usize % self.shards.len()].lock(); + shard.remove(hash, key) + }; + + // Do not deallocate data within the lock section. + if let Some((key, value, context, charges)) = entry { + self.context.listener.on_release(key, value, context.into(), charges); + } + } + + pub fn get(self: &Arc, key: &Q) -> Option> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let hash = self.hash_builder.hash_one(key); + + unsafe { + let mut shard = self.shards[hash as usize % self.shards.len()].lock(); + shard.get(hash, key).map(|ptr| GenericCacheEntry { + cache: self.clone(), + ptr, + }) + } + } + + pub fn contains(self: &Arc, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let hash = self.hash_builder.hash_one(key); + + unsafe { + let mut shard = self.shards[hash as usize % self.shards.len()].lock(); + shard.contains(hash, key) + } + } + + pub fn clear(&self) { + let mut to_deallocate = vec![]; + for shard in self.shards.iter() { + let mut shard = shard.lock(); + unsafe { shard.clear(&mut to_deallocate) }; + } + } + + pub fn capacity(&self) -> usize { + self.capacity + } + + pub fn usage(&self) -> usize { + self.usages.iter().map(|usage| usage.load(Ordering::Relaxed)).sum() + } + + pub fn metrics(&self) -> &Metrics { + &self.context.metrics + } + + unsafe fn try_release_external_handle(&self, ptr: NonNull) { + let entry = { + let base = ptr.as_ref().base(); + let mut shard = self.shards[base.hash() as usize % self.shards.len()].lock(); + shard.try_release_external_handle(ptr) + }; + + // Do not deallocate data within the lock section. + if let Some((key, value, context, charges)) = entry { + self.context.listener.on_release(key, value, context.into(), charges); + } + } +} + +// TODO(MrCroxx): use `hashbrown::HashTable` with `Handle` may relax the `Clone` bound? +impl GenericCache +where + K: Key + Clone, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn entry(self: &Arc, key: K, f: F) -> GenericEntry + where + F: FnOnce() -> FU, + FU: Future> + Send + 'static, + ER: std::error::Error + Send + 'static, + { + let hash = self.hash_builder.hash_one(&key); + + unsafe { + let mut shard = self.shards[hash as usize % self.shards.len()].lock(); + if let Some(ptr) = shard.get(hash, &key) { + return GenericEntry::Hit(GenericCacheEntry { + cache: self.clone(), + ptr, + }); + } + let entry = match shard.waiters.entry(key.clone()) { + HashMapEntry::Occupied(mut o) => { + let (tx, rx) = oneshot::channel(); + o.get_mut().push(tx); + GenericEntry::Wait(rx) + } + HashMapEntry::Vacant(v) => { + v.insert(vec![]); + let cache = self.clone(); + let future = f(); + let join = tokio::spawn(async move { + let (value, charge, context) = match future.await { + Ok((value, charge, context)) => (value, charge, context), + Err(e) => { + let mut shard = cache.shards[hash as usize % cache.shards.len()].lock(); + shard.waiters.remove(&key); + return Err(e); + } + }; + let entry = cache.insert_with_context(key, value, charge, context); + Ok(entry) + }); + GenericEntry::Miss(join) + } + }; + match entry { + GenericEntry::Wait(_) => shard.state.metrics.queue.fetch_add(1, Ordering::Relaxed), + GenericEntry::Miss(_) => shard.state.metrics.fetch.fetch_add(1, Ordering::Relaxed), + _ => unreachable!(), + }; + entry + } + } +} + +pub struct GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + cache: Arc>, + ptr: NonNull, +} + +impl GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + pub fn key(&self) -> &K { + unsafe { &self.ptr.as_ref().base().data_unwrap_unchecked().0 } + } + + pub fn value(&self) -> &V { + unsafe { &self.ptr.as_ref().base().data_unwrap_unchecked().1 } + } + + pub fn context(&self) -> &::Context { + unsafe { self.ptr.as_ref().base().context() } + } + + pub fn charge(&self) -> usize { + unsafe { self.ptr.as_ref().base().charge() } + } + + pub fn refs(&self) -> usize { + unsafe { self.ptr.as_ref().base().refs() } + } +} + +impl Clone for GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn clone(&self) -> Self { + let mut ptr = self.ptr; + + unsafe { + let base = ptr.as_mut().base_mut(); + debug_assert!(base.has_refs()); + base.inc_refs(); + } + + Self { + cache: self.cache.clone(), + ptr, + } + } +} + +impl Drop for GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + fn drop(&mut self) { + unsafe { self.cache.try_release_external_handle(self.ptr) } + } +} + +impl Deref for GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ + type Target = V; + + fn deref(&self) -> &Self::Target { + self.value() + } +} + +unsafe impl Send for GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ +} +unsafe impl Sync for GenericCacheEntry +where + K: Key, + V: Value, + E: Eviction, + E::Item: KeyedHandle, + I: Indexer, + L: CacheEventListener, + S: BuildHasher + Send + Sync + 'static, +{ +} + +#[cfg(test)] +mod tests { + use rand::{rngs::SmallRng, RngCore, SeedableRng}; + + use super::*; + use crate::{ + cache::{FifoCache, FifoCacheConfig, FifoCacheEntry, LruCache, LruCacheConfig, LruCacheEntry}, + eviction::{ + fifo::{FifoConfig, FifoHandle}, + lru::LruConfig, + test_utils::TestEviction, + }, + listener::DefaultCacheEventListener, + }; + + fn is_send_sync_static() {} + + #[test] + fn test_send_sync_static() { + is_send_sync_static::>(); + is_send_sync_static::>(); + is_send_sync_static::>(); + is_send_sync_static::>(); + } + + #[test] + fn test_cache_fuzzy() { + const CAPACITY: usize = 256; + + let config = FifoCacheConfig { + capacity: CAPACITY, + shards: 4, + eviction_config: FifoConfig {}, + object_pool_capacity: 16, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + let cache = Arc::new(FifoCache::::new(config)); + + let mut rng = SmallRng::seed_from_u64(114514); + for _ in 0..100000 { + let key = rng.next_u64(); + if let Some(entry) = cache.get(&key) { + assert_eq!(key, *entry); + drop(entry); + continue; + } + cache.insert(key, key, 1); + } + assert_eq!(cache.usage(), CAPACITY); + } + + fn fifo(capacity: usize) -> Arc> { + let config = FifoCacheConfig { + capacity, + shards: 1, + eviction_config: FifoConfig {}, + object_pool_capacity: 1, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + Arc::new(FifoCache::::new(config)) + } + + fn lru(capacity: usize) -> Arc> { + let config = LruCacheConfig { + capacity, + shards: 1, + eviction_config: LruConfig { + high_priority_pool_ratio: 0.0, + }, + object_pool_capacity: 1, + hash_builder: RandomState::default(), + event_listener: DefaultCacheEventListener::default(), + }; + Arc::new(LruCache::::new(config)) + } + + fn insert_fifo(cache: &Arc>, key: u64, value: &str) -> FifoCacheEntry { + cache.insert(key, value.to_string(), value.len()) + } + + fn insert_lru(cache: &Arc>, key: u64, value: &str) -> LruCacheEntry { + cache.insert(key, value.to_string(), value.len()) + } + + #[test] + fn test_reference_count() { + let cache = fifo(100); + + let refs = |ptr: NonNull>| unsafe { ptr.as_ref().base().refs() }; + + let e1 = insert_fifo(&cache, 42, "the answer to life, the universe, and everything"); + let ptr = e1.ptr; + assert_eq!(refs(ptr), 1); + + let e2 = cache.get(&42).unwrap(); + assert_eq!(refs(ptr), 2); + + let e3 = e2.clone(); + assert_eq!(refs(ptr), 3); + + drop(e2); + assert_eq!(refs(ptr), 2); + + drop(e3); + assert_eq!(refs(ptr), 1); + + drop(e1); + assert_eq!(refs(ptr), 0); + } + + #[test] + fn test_replace() { + let cache = fifo(10); + + insert_fifo(&cache, 114, "xx"); + assert_eq!(cache.usage(), 2); + + insert_fifo(&cache, 514, "QwQ"); + assert_eq!(cache.usage(), 5); + + insert_fifo(&cache, 114, "(0.0)"); + assert_eq!(cache.usage(), 8); + + assert_eq!( + cache.shards[0].lock().eviction.dump(), + vec![(514, "QwQ".to_string()), (114, "(0.0)".to_string())], + ); + } + + #[test] + fn test_replace_with_external_refs() { + let cache = fifo(10); + + insert_fifo(&cache, 514, "QwQ"); + insert_fifo(&cache, 114, "(0.0)"); + + let e4 = cache.get(&514).unwrap(); + let e5 = insert_fifo(&cache, 514, "bili"); + + assert_eq!(e4.refs(), 1); + assert_eq!(e5.refs(), 1); + + // remains: 514 => QwQ (3), 514 => bili (4) + // evicted: 114 => (0.0) (5) + assert_eq!(cache.usage(), 7); + + assert!(cache.get(&114).is_none()); + assert_eq!(cache.get(&514).unwrap().value(), "bili"); + assert_eq!(e4.value(), "QwQ"); + + cache.remove(&514); + assert_eq!(e5.value(), "bili"); + + drop(e5); + assert!(cache.get(&514).is_none()); + assert_eq!(e4.value(), "QwQ"); + + assert_eq!(cache.usage(), 3); + drop(e4); + assert_eq!(cache.usage(), 0); + } + + #[test] + fn test_reinsert_while_all_referenced_lru() { + let cache = lru(10); + + let e1 = insert_lru(&cache, 1, "111"); + let e2 = insert_lru(&cache, 2, "222"); + let e3 = insert_lru(&cache, 3, "333"); + assert_eq!(cache.usage(), 9); + + // No entry will be released because all of them are referenced externally. + let e4 = insert_lru(&cache, 4, "444"); + assert_eq!(cache.usage(), 12); + + // `111`, `222` and `333` are evicted from the eviction container to make space for `444`. + assert_eq!(cache.shards[0].lock().eviction.dump(), vec![(4, "444".to_string()),]); + + // `e1` cannot be reinserted for the usage has already exceeds the capacity. + drop(e1); + assert_eq!(cache.usage(), 9); + + // `222` and `333` will be reinserted + drop(e2); + drop(e3); + assert_eq!( + cache.shards[0].lock().eviction.dump(), + vec![(4, "444".to_string()), (2, "222".to_string()), (3, "333".to_string()),] + ); + assert_eq!(cache.usage(), 9); + + // `444` will be reinserted + drop(e4); + assert_eq!( + cache.shards[0].lock().eviction.dump(), + vec![(2, "222".to_string()), (3, "333".to_string()), (4, "444".to_string()),] + ); + assert_eq!(cache.usage(), 9); + } + + #[test] + fn test_reinsert_while_all_referenced_fifo() { + let cache = fifo(10); + + let e1 = insert_fifo(&cache, 1, "111"); + let e2 = insert_fifo(&cache, 2, "222"); + let e3 = insert_fifo(&cache, 3, "333"); + assert_eq!(cache.usage(), 9); + + // No entry will be released because all of them are referenced externally. + let e4 = insert_fifo(&cache, 4, "444"); + assert_eq!(cache.usage(), 12); + + // `111`, `222` and `333` are evicted from the eviction container to make space for `444`. + assert_eq!(cache.shards[0].lock().eviction.dump(), vec![(4, "444".to_string()),]); + + // `e1` cannot be reinserted for the usage has already exceeds the capacity. + drop(e1); + assert_eq!(cache.usage(), 9); + + // `222` and `333` will be not reinserted because fifo will ignore reinsert operations. + drop([e2, e3, e4]); + assert_eq!(cache.shards[0].lock().eviction.dump(), vec![(4, "444".to_string()),]); + assert_eq!(cache.usage(), 3); + + // Note: + // + // For cache policy like FIFO, the entries will not be reinserted while all handles are referenced. + // It's okay for this is not a common situation and is not supposed to happen in real workload. + } +} diff --git a/foyer-memory/src/handle.rs b/foyer-memory/src/handle.rs new file mode 100644 index 00000000..9e81e86e --- /dev/null +++ b/foyer-memory/src/handle.rs @@ -0,0 +1,238 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bitflags::bitflags; + +use crate::{context::Context, Key, Value}; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct BaseHandleFlags: u8 { + const IN_INDEXER = 0b00000001; + const IN_EVICTION = 0b00000010; + } +} + +pub trait Handle: Send + Sync + 'static { + type Data; + type Context: Context; + + fn new() -> Self; + fn init(&mut self, hash: u64, data: Self::Data, charge: usize, context: Self::Context); + + fn base(&self) -> &BaseHandle; + fn base_mut(&mut self) -> &mut BaseHandle; +} + +pub trait KeyedHandle: Handle { + type Key; + + fn key(&self) -> &Self::Key; +} + +impl KeyedHandle for T +where + K: Key, + V: Value, + T: Handle, +{ + type Key = K; + + fn key(&self) -> &Self::Key { + &self.base().data_unwrap_unchecked().0 + } +} + +#[derive(Debug)] +pub struct BaseHandle { + /// key, value, context + entry: Option<(T, C)>, + /// key hash + hash: u64, + /// entry charge + charge: usize, + /// external reference count + refs: usize, + /// flags that used by the general cache abstraction + flags: BaseHandleFlags, +} + +impl Default for BaseHandle { + fn default() -> Self { + Self::new() + } +} + +impl BaseHandle { + /// Create a uninited handle. + #[inline(always)] + pub fn new() -> Self { + Self { + entry: None, + hash: 0, + charge: 0, + refs: 0, + flags: BaseHandleFlags::empty(), + } + } + + /// Init handle with args. + #[inline(always)] + pub fn init(&mut self, hash: u64, data: T, charge: usize, context: C) { + debug_assert!(self.entry.is_none()); + self.hash = hash; + self.entry = Some((data, context)); + self.charge = charge; + self.refs = 0; + self.flags = BaseHandleFlags::empty(); + } + + /// Take key and value from the handle and reset it to the uninited state. + #[inline(always)] + pub fn take(&mut self) -> (T, C, usize) { + debug_assert!(self.entry.is_some()); + unsafe { + self.entry + .take() + .map(|(data, context)| (data, context, self.charge)) + .unwrap_unchecked() + } + } + + /// Return `true` if the handle is inited. + #[inline(always)] + pub fn is_inited(&self) -> bool { + self.entry.is_some() + } + + /// Get key hash. + /// + /// # Panics + /// + /// Panics if the handle is uninited. + #[inline(always)] + pub fn hash(&self) -> u64 { + self.hash + } + + /// Get data reference. + /// + /// # Panics + /// + /// Panics if the handle is uninited. + #[inline(always)] + pub fn data_unwrap_unchecked(&self) -> &T { + debug_assert!(self.entry.is_some()); + unsafe { self.entry.as_ref().map(|entry| &entry.0).unwrap_unchecked() } + } + + /// Get context reference. + /// + /// # Panics + /// + /// Panics if the handle is uninited. + #[inline(always)] + pub fn context(&self) -> &C { + debug_assert!(self.entry.is_some()); + unsafe { self.entry.as_ref().map(|entry| &entry.1).unwrap_unchecked() } + } + + /// Get the charge of the handle. + #[inline(always)] + pub fn charge(&self) -> usize { + self.charge + } + + /// Increase the external reference count of the handle, returns the new reference count. + #[inline(always)] + pub fn inc_refs(&mut self) -> usize { + self.inc_refs_by(1) + } + + /// Increase the external reference count of the handle, returns the new reference count. + #[inline(always)] + pub fn inc_refs_by(&mut self, val: usize) -> usize { + self.refs += val; + self.refs + } + + /// Decrease the external reference count of the handle, returns the new reference count. + #[inline(always)] + pub fn dec_refs(&mut self) -> usize { + self.refs -= 1; + self.refs + } + + /// Get the external reference count of the handle. + #[inline(always)] + pub fn refs(&self) -> usize { + self.refs + } + + /// Return `true` if there are external references. + #[inline(always)] + pub fn has_refs(&self) -> bool { + self.refs() > 0 + } + + #[inline(always)] + pub fn set_in_indexer(&mut self, in_cache: bool) { + if in_cache { + self.flags |= BaseHandleFlags::IN_INDEXER; + } else { + self.flags -= BaseHandleFlags::IN_INDEXER; + } + } + + #[inline(always)] + pub fn is_in_indexer(&self) -> bool { + !(self.flags & BaseHandleFlags::IN_INDEXER).is_empty() + } + + #[inline(always)] + pub fn set_in_eviction(&mut self, in_eviction: bool) { + if in_eviction { + self.flags |= BaseHandleFlags::IN_EVICTION; + } else { + self.flags -= BaseHandleFlags::IN_EVICTION; + } + } + + #[inline(always)] + pub fn is_in_eviction(&self) -> bool { + !(self.flags & BaseHandleFlags::IN_EVICTION).is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base_handle_basic() { + let mut h = BaseHandle::<(), ()>::new(); + assert!(!h.is_in_indexer()); + assert!(!h.is_in_eviction()); + + h.set_in_indexer(true); + h.set_in_eviction(true); + assert!(h.is_in_indexer()); + assert!(h.is_in_eviction()); + + h.set_in_indexer(false); + h.set_in_eviction(false); + assert!(!h.is_in_indexer()); + assert!(!h.is_in_eviction()); + } +} diff --git a/foyer-memory/src/indexer.rs b/foyer-memory/src/indexer.rs new file mode 100644 index 00000000..0fbf2f38 --- /dev/null +++ b/foyer-memory/src/indexer.rs @@ -0,0 +1,133 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{borrow::Borrow, hash::Hash, ptr::NonNull}; + +use hashbrown::hash_table::{Entry as HashTableEntry, HashTable}; + +use crate::{handle::KeyedHandle, Key}; + +pub trait Indexer: Send + Sync + 'static { + type Key: Key; + type Handle: KeyedHandle; + + fn new() -> Self; + unsafe fn insert(&mut self, handle: NonNull) -> Option>; + unsafe fn get(&self, hash: u64, key: &Q) -> Option> + where + Self::Key: Borrow, + Q: Hash + Eq + ?Sized; + unsafe fn remove(&mut self, hash: u64, key: &Q) -> Option> + where + Self::Key: Borrow, + Q: Hash + Eq + ?Sized; + unsafe fn drain(&mut self) -> impl Iterator>; +} + +pub struct HashTableIndexer +where + K: Key, + H: KeyedHandle, +{ + table: HashTable>, +} + +unsafe impl Send for HashTableIndexer +where + K: Key, + H: KeyedHandle, +{ +} + +unsafe impl Sync for HashTableIndexer +where + K: Key, + H: KeyedHandle, +{ +} + +impl Indexer for HashTableIndexer +where + K: Key, + H: KeyedHandle, +{ + type Key = K; + type Handle = H; + + fn new() -> Self { + Self { + table: HashTable::new(), + } + } + + unsafe fn insert(&mut self, mut ptr: NonNull) -> Option> { + let handle = ptr.as_mut(); + + debug_assert!(!handle.base().is_in_indexer()); + handle.base_mut().set_in_indexer(true); + + match self.table.entry( + handle.base().hash(), + |p| p.as_ref().key() == handle.key(), + |p| p.as_ref().base().hash(), + ) { + HashTableEntry::Occupied(mut o) => { + std::mem::swap(o.get_mut(), &mut ptr); + let b = ptr.as_mut().base_mut(); + debug_assert!(b.is_in_indexer()); + b.set_in_indexer(false); + Some(ptr) + } + HashTableEntry::Vacant(v) => { + v.insert(ptr); + None + } + } + } + + unsafe fn get(&self, hash: u64, key: &Q) -> Option> + where + Self::Key: Borrow, + Q: Hash + Eq + ?Sized, + { + self.table.find(hash, |p| p.as_ref().key().borrow() == key).copied() + } + + unsafe fn remove(&mut self, hash: u64, key: &Q) -> Option> + where + Self::Key: Borrow, + Q: Hash + Eq + ?Sized, + { + match self + .table + .entry(hash, |p| p.as_ref().key().borrow() == key, |p| p.as_ref().base().hash()) + { + HashTableEntry::Occupied(o) => { + let (mut p, _) = o.remove(); + let b = p.as_mut().base_mut(); + debug_assert!(b.is_in_indexer()); + b.set_in_indexer(false); + Some(p) + } + HashTableEntry::Vacant(_) => None, + } + } + + unsafe fn drain(&mut self) -> impl Iterator> { + self.table.drain().map(|mut ptr| { + ptr.as_mut().base_mut().set_in_indexer(false); + ptr + }) + } +} diff --git a/foyer-memory/src/lib.rs b/foyer-memory/src/lib.rs new file mode 100644 index 00000000..fe0e8ae6 --- /dev/null +++ b/foyer-memory/src/lib.rs @@ -0,0 +1,81 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This crate provides a concurrent in-memory cache component that supports replaceable eviction algorithm. +//! +//! # Motivation +//! +//! There are a few goals to achieve with the crate: +//! +//! 1. Pluggable eviction algorithm with the same abstraction. +//! 2. Tracking the real memory usage by the cache. Including both holding by the cache and by the external users. +//! 3. Reduce the concurrent read-through requests into one. +//! +//! To achieve them, the crate needs to combine the advantages of the implementations of RocksDB and CacheLib. +//! +//! # Design +//! +//! The cache is mainly compused of the following components: +//! 1. handle : Carries the cached entry, reference count, pointer links in the eviction container, etc. +//! 2. indexer : Indexes cached keys to the handles. +//! 3. eviction container : Defines the order of eviction. Usually implemented with intrusive data structures. +//! +//! Because a handle needs to be referenced and mutated by both the indexer and the eviction container in the same +//! thread, it is hard to implement in 100% safe Rust without overhead. So, the APIs of the indexer and the eviciton +//! container are defined with `NonNull` pointers of the handles. +//! +//! When some entry is inserted into the cache, the associated handle should be transmuted into pointer without +//! dropping. When some entry is removed from the cache, the pointer of the associated handle should be transmuted into +//! an owned data structure. +//! +//! # Handle Lifetime +//! +//! The handle is created during a new entry is being inserted, and then inserted into both the indexer and the eviction +//! container. +//! +//! The handle is return if the entry is retrieved from the cache. The handle will track the count of the external +//! owners to decide the time to reclaim. +//! +//! When a key is removed or updated, the original handle will be removed from the indexer and the eviction container, +//! and waits to be released by all the external owners before reclamation. +//! +//! When the cache is full and being inserted, a handle will be evicted from the eviction container based on the +//! eviction algorithm. The evicted handle will NOT be removed from the indexer immediately because it still occupies +//! memory and can be used by queries followed up. +//! +//! After the handle is released by all the external owners, the eviction container will update its order or evict it +//! based on the eviction algorithm. If it doesn't appear in the eviction container, it may be reinserted if it still in +//! the indexer and there is enough space. Otherwise, it will be removed from both the indexer and the eviction +//! container. +//! +//! The handle that does not appear in either the indexer or the eviction container, and has no external owner, will be +//! destroyed. + +pub trait Key: Send + Sync + 'static + std::hash::Hash + Eq + Ord {} +pub trait Value: Send + Sync + 'static {} + +impl Key for T {} +impl Value for T {} + +mod cache; +mod context; +mod eviction; +mod generic; +mod handle; +mod indexer; +mod listener; +mod metrics; +mod prelude; + +pub use prelude::*; diff --git a/foyer-memory/src/listener.rs b/foyer-memory/src/listener.rs new file mode 100644 index 00000000..6099bfb3 --- /dev/null +++ b/foyer-memory/src/listener.rs @@ -0,0 +1,51 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::marker::PhantomData; + +use crate::{CacheContext, Key, Value}; + +pub trait CacheEventListener: Send + Sync + 'static +where + K: Key, + V: Value, +{ + /// The function is called when an entry is released by the cache and all external users. + /// + /// The arguments includes the key and value with ownership. + fn on_release(&self, key: K, value: V, context: CacheContext, charges: usize); +} + +pub struct DefaultCacheEventListener(PhantomData<(K, V)>) +where + K: Key, + V: Value; + +impl Default for DefaultCacheEventListener +where + K: Key, + V: Value, +{ + fn default() -> Self { + Self(Default::default()) + } +} + +impl CacheEventListener for DefaultCacheEventListener +where + K: Key, + V: Value, +{ + fn on_release(&self, _key: K, _value: V, _context: CacheContext, _charges: usize) {} +} diff --git a/foyer-memory/src/metrics.rs b/foyer-memory/src/metrics.rs new file mode 100644 index 00000000..4644730b --- /dev/null +++ b/foyer-memory/src/metrics.rs @@ -0,0 +1,44 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::atomic::AtomicUsize; + +#[derive(Debug, Default)] +pub struct Metrics { + /// successful inserts without replaces + pub insert: AtomicUsize, + /// successful replaces + pub replace: AtomicUsize, + + /// get hits + pub hit: AtomicUsize, + /// get misses + pub miss: AtomicUsize, + + /// fetches after cache miss with `entry` interface + pub fetch: AtomicUsize, + /// deduped fetches after cache miss with `entry` interface + pub queue: AtomicUsize, + + /// successful removes + pub remove: AtomicUsize, + + /// evicts from the eviction container + pub evict: AtomicUsize, + /// successful reinserts, only counts successful reinserts after evicted + pub reinsert: AtomicUsize, + + /// released handles + pub release: AtomicUsize, +} diff --git a/foyer-memory/src/prelude.rs b/foyer-memory/src/prelude.rs new file mode 100644 index 00000000..5ae7c4d0 --- /dev/null +++ b/foyer-memory/src/prelude.rs @@ -0,0 +1,21 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use crate::{ + cache::{Cache, CacheEntry, Entry, EntryState, FifoCacheConfig, LfuCacheConfig, LruCacheConfig, S3FifoCacheConfig}, + context::CacheContext, + eviction::{fifo::FifoConfig, lfu::LfuConfig, lru::LruConfig, s3fifo::S3FifoConfig}, + listener::{CacheEventListener, DefaultCacheEventListener}, + metrics::Metrics, +}; diff --git a/foyer-storage-bench/Cargo.toml b/foyer-storage-bench/Cargo.toml new file mode 100644 index 00000000..c7f02b49 --- /dev/null +++ b/foyer-storage-bench/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "foyer-storage-bench" +version = "0.6.0" +edition = "2021" +authors = ["MrCroxx "] +description = "storage engine bench tool for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +anyhow = "1" +bytesize = "1" +clap = { version = "4", features = ["derive"] } +console-subscriber = { version = "0.2", optional = true } +foyer-common = { version = "0.5", path = "../foyer-common" } +foyer-intrusive = { version = "0.4", path = "../foyer-intrusive" } +foyer-storage = { version = "0.6", path = "../foyer-storage" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +futures = "0.3" +hdrhistogram = "7" +http-body-util = "0.1" +hyper = { version = "1.0", features = ["server", "http1", "http2"] } +hyper-util = { version = "0.1", features = [ + "server", + "server-auto", + "http1", + "http2", + "tokio", +] } +itertools = "0.12" +libc = "0.2" +nix = { version = "0.28", features = ["fs", "mman"] } +opentelemetry = { version = "0.22", optional = true } +opentelemetry-otlp = { version = "0.15.0", optional = true } +opentelemetry-semantic-conventions = { version = "0.14", optional = true } +opentelemetry_sdk = { version = "0.22", features = [ + "rt-tokio", + "trace", +], optional = true } +parking_lot = "0.12" +prometheus = "0.13" +rand = "0.8.5" +tokio = { workspace = true } +tracing = "0.1" +tracing-opentelemetry = { version = "0.23", optional = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +zipf = "7" + +[features] +deadlock = ["parking_lot/deadlock_detection", "foyer-storage/deadlock"] +tokio-console = ["console-subscriber"] +trace = [ + "opentelemetry", + "opentelemetry_sdk", + "opentelemetry-otlp", + "tracing-opentelemetry", + "opentelemetry-semantic-conventions", +] diff --git a/foyer-storage-bench/etc/sample.txt b/foyer-storage-bench/etc/sample.txt new file mode 100644 index 00000000..ab729b88 --- /dev/null +++ b/foyer-storage-bench/etc/sample.txt @@ -0,0 +1,676 @@ +!!! This is just a sample text, not a real license. !!! + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/foyer-storage-bench/src/analyze.rs b/foyer-storage-bench/src/analyze.rs new file mode 100644 index 00000000..4c50cfa5 --- /dev/null +++ b/foyer-storage-bench/src/analyze.rs @@ -0,0 +1,333 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + path::Path, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use bytesize::ByteSize; +use hdrhistogram::Histogram; +use parking_lot::RwLock; +use tokio::sync::broadcast; + +use crate::utils::{iostat, IoStat}; + +const SECTOR_SIZE: usize = 512; + +// latencies are measured by 'us' +#[derive(Clone, Copy, Debug)] +pub struct Analysis { + disk_read_iops: f64, + disk_read_throughput: f64, + disk_write_iops: f64, + disk_write_throughput: f64, + + insert_iops: f64, + insert_throughput: f64, + insert_lat_p50: u64, + insert_lat_p90: u64, + insert_lat_p99: u64, + insert_lat_p999: u64, + insert_lat_p9999: u64, + insert_lat_p99999: u64, + insert_lat_pmax: u64, + + get_iops: f64, + get_miss: f64, + get_throughput: f64, + get_miss_lat_p50: u64, + get_miss_lat_p90: u64, + get_miss_lat_p99: u64, + get_miss_lat_p999: u64, + get_miss_lat_p9999: u64, + get_miss_lat_p99999: u64, + get_miss_lat_pmax: u64, + get_hit_lat_p50: u64, + get_hit_lat_p90: u64, + get_hit_lat_p99: u64, + get_hit_lat_p999: u64, + get_hit_lat_p9999: u64, + get_hit_lat_p99999: u64, + get_hit_lat_pmax: u64, +} + +#[derive(Default, Clone, Copy, Debug)] +pub struct MetricsDump { + pub insert_ios: usize, + pub insert_bytes: usize, + pub insert_lat_p50: u64, + pub insert_lat_p90: u64, + pub insert_lat_p99: u64, + pub insert_lat_p999: u64, + pub insert_lat_p9999: u64, + pub insert_lat_p99999: u64, + pub insert_lat_pmax: u64, + + pub get_ios: usize, + pub get_miss_ios: usize, + pub get_bytes: usize, + pub get_hit_lat_p50: u64, + pub get_hit_lat_p90: u64, + pub get_hit_lat_p99: u64, + pub get_hit_lat_p999: u64, + pub get_hit_lat_p9999: u64, + pub get_hit_lat_p99999: u64, + pub get_hit_lat_pmax: u64, + pub get_miss_lat_p50: u64, + pub get_miss_lat_p90: u64, + pub get_miss_lat_p99: u64, + pub get_miss_lat_p999: u64, + pub get_miss_lat_p9999: u64, + pub get_miss_lat_p99999: u64, + pub get_miss_lat_pmax: u64, +} + +#[derive(Clone, Debug)] +pub struct Metrics { + pub insert_ios: Arc, + pub insert_bytes: Arc, + pub insert_lats: Arc>>, + + pub get_ios: Arc, + pub get_bytes: Arc, + pub get_miss_ios: Arc, + pub get_hit_lats: Arc>>, + pub get_miss_lats: Arc>>, +} + +impl Default for Metrics { + fn default() -> Self { + Self { + insert_ios: Arc::new(AtomicUsize::new(0)), + insert_bytes: Arc::new(AtomicUsize::new(0)), + insert_lats: Arc::new(RwLock::new(Histogram::new_with_bounds(1, 10_000_000, 2).unwrap())), + + get_ios: Arc::new(AtomicUsize::new(0)), + get_bytes: Arc::new(AtomicUsize::new(0)), + get_miss_ios: Arc::new(AtomicUsize::new(0)), + get_hit_lats: Arc::new(RwLock::new(Histogram::new_with_bounds(1, 10_000_000, 2).unwrap())), + get_miss_lats: Arc::new(RwLock::new(Histogram::new_with_bounds(1, 10_000_000, 2).unwrap())), + } + } +} + +impl Metrics { + pub fn dump(&self) -> MetricsDump { + let insert_lats = self.insert_lats.read(); + let get_hit_lats = self.get_hit_lats.read(); + let get_miss_lats = self.get_miss_lats.read(); + + MetricsDump { + insert_ios: self.insert_ios.load(Ordering::Relaxed), + insert_bytes: self.insert_bytes.load(Ordering::Relaxed), + insert_lat_p50: insert_lats.value_at_quantile(0.5), + insert_lat_p90: insert_lats.value_at_quantile(0.9), + insert_lat_p99: insert_lats.value_at_quantile(0.99), + insert_lat_p999: insert_lats.value_at_quantile(0.999), + insert_lat_p9999: insert_lats.value_at_quantile(0.9999), + insert_lat_p99999: insert_lats.value_at_quantile(0.99999), + insert_lat_pmax: insert_lats.max(), + + get_ios: self.get_ios.load(Ordering::Relaxed), + get_miss_ios: self.get_miss_ios.load(Ordering::Relaxed), + get_bytes: self.get_bytes.load(Ordering::Relaxed), + get_hit_lat_p50: get_hit_lats.value_at_quantile(0.5), + get_hit_lat_p90: get_hit_lats.value_at_quantile(0.9), + get_hit_lat_p99: get_hit_lats.value_at_quantile(0.99), + get_hit_lat_p999: get_hit_lats.value_at_quantile(0.999), + get_hit_lat_p9999: get_hit_lats.value_at_quantile(0.9999), + get_hit_lat_p99999: get_hit_lats.value_at_quantile(0.99999), + get_hit_lat_pmax: get_hit_lats.max(), + get_miss_lat_p50: get_miss_lats.value_at_quantile(0.5), + get_miss_lat_p90: get_miss_lats.value_at_quantile(0.9), + get_miss_lat_p99: get_miss_lats.value_at_quantile(0.99), + get_miss_lat_p999: get_miss_lats.value_at_quantile(0.999), + get_miss_lat_p9999: get_miss_lats.value_at_quantile(0.9999), + get_miss_lat_p99999: get_miss_lats.value_at_quantile(0.99999), + get_miss_lat_pmax: get_miss_lats.max(), + } + } +} + +impl std::fmt::Display for Analysis { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let disk_read_throughput = ByteSize::b(self.disk_read_throughput as u64); + let disk_write_throughput = ByteSize::b(self.disk_write_throughput as u64); + let disk_total_throughput = disk_read_throughput + disk_write_throughput; + + // disk statics + writeln!(f, "disk total iops: {:.1}", self.disk_write_iops + self.disk_read_iops)?; + writeln!( + f, + "disk total throughput: {}/s", + disk_total_throughput.to_string_as(true) + )?; + writeln!(f, "disk read iops: {:.1}", self.disk_read_iops)?; + writeln!(f, "disk read throughput: {}/s", disk_read_throughput.to_string_as(true))?; + writeln!(f, "disk write iops: {:.1}", self.disk_write_iops)?; + writeln!( + f, + "disk write throughput: {}/s", + disk_write_throughput.to_string_as(true) + )?; + + // insert statics + let insert_throughput = ByteSize::b(self.insert_throughput as u64); + writeln!(f, "insert iops: {:.1}/s", self.insert_iops)?; + writeln!(f, "insert throughput: {}/s", insert_throughput.to_string_as(true))?; + writeln!(f, "insert lat p50: {}us", self.insert_lat_p50)?; + writeln!(f, "insert lat p90: {}us", self.insert_lat_p90)?; + writeln!(f, "insert lat p99: {}us", self.insert_lat_p99)?; + writeln!(f, "insert lat p999: {}us", self.insert_lat_p999)?; + writeln!(f, "insert lat p9999: {}us", self.insert_lat_p9999)?; + writeln!(f, "insert lat p99999: {}us", self.insert_lat_p99999)?; + writeln!(f, "insert lat pmax: {}us", self.insert_lat_pmax)?; + + // get statics + let get_throughput = ByteSize::b(self.get_throughput as u64); + writeln!(f, "get iops: {:.1}/s", self.get_iops)?; + writeln!(f, "get miss: {:.2}% ", self.get_miss * 100f64)?; + writeln!(f, "get throughput: {}/s", get_throughput.to_string_as(true))?; + writeln!(f, "get hit lat p50: {}us", self.get_hit_lat_p50)?; + writeln!(f, "get hit lat p90: {}us", self.get_hit_lat_p90)?; + writeln!(f, "get hit lat p99: {}us", self.get_hit_lat_p99)?; + writeln!(f, "get hit lat p999: {}us", self.get_hit_lat_p999)?; + writeln!(f, "get hit lat p9999: {}us", self.get_hit_lat_p9999)?; + writeln!(f, "get hit lat p99999: {}us", self.get_hit_lat_p99999)?; + writeln!(f, "get hit lat pmax: {}us", self.get_hit_lat_pmax)?; + writeln!(f, "get miss lat p50: {}us", self.get_miss_lat_p50)?; + writeln!(f, "get miss lat p90: {}us", self.get_miss_lat_p90)?; + writeln!(f, "get miss lat p99: {}us", self.get_miss_lat_p99)?; + writeln!(f, "get miss lat p999: {}us", self.get_miss_lat_p999)?; + writeln!(f, "get miss lat p9999: {}us", self.get_miss_lat_p9999)?; + writeln!(f, "get miss lat p99999: {}us", self.get_miss_lat_p99999)?; + writeln!(f, "get miss lat pmax: {}us", self.get_miss_lat_pmax)?; + + Ok(()) + } +} + +pub fn analyze( + duration: Duration, + iostat_start: &IoStat, + iostat_end: &IoStat, + metrics_dump_start: &MetricsDump, + metrics_dump_end: &MetricsDump, +) -> Analysis { + let secs = duration.as_secs_f64(); + let disk_read_iops = (iostat_end.read_ios - iostat_start.read_ios) as f64 / secs; + let disk_read_throughput = (iostat_end.read_sectors - iostat_start.read_sectors) as f64 * SECTOR_SIZE as f64 / secs; + let disk_write_iops = (iostat_end.write_ios - iostat_start.write_ios) as f64 / secs; + let disk_write_throughput = + (iostat_end.write_sectors - iostat_start.write_sectors) as f64 * SECTOR_SIZE as f64 / secs; + + let insert_iops = (metrics_dump_end.insert_ios - metrics_dump_start.insert_ios) as f64 / secs; + let insert_throughput = (metrics_dump_end.insert_bytes - metrics_dump_start.insert_bytes) as f64 / secs; + + let get_iops = (metrics_dump_end.get_ios - metrics_dump_start.get_ios) as f64 / secs; + let get_miss = (metrics_dump_end.get_miss_ios - metrics_dump_start.get_miss_ios) as f64 + / (metrics_dump_end.get_ios - metrics_dump_start.get_ios) as f64; + let get_throughput = (metrics_dump_end.get_bytes - metrics_dump_start.get_bytes) as f64 / secs; + + Analysis { + disk_read_iops, + disk_read_throughput, + disk_write_iops, + disk_write_throughput, + + insert_iops, + insert_throughput, + insert_lat_p50: metrics_dump_end.insert_lat_p50, + insert_lat_p90: metrics_dump_end.insert_lat_p90, + insert_lat_p99: metrics_dump_end.insert_lat_p99, + insert_lat_p999: metrics_dump_end.insert_lat_p999, + insert_lat_p9999: metrics_dump_end.insert_lat_p9999, + insert_lat_p99999: metrics_dump_end.insert_lat_p99999, + insert_lat_pmax: metrics_dump_end.insert_lat_pmax, + + get_iops, + get_miss, + get_throughput, + get_hit_lat_p50: metrics_dump_end.get_hit_lat_p50, + get_hit_lat_p90: metrics_dump_end.get_hit_lat_p90, + get_hit_lat_p99: metrics_dump_end.get_hit_lat_p99, + get_hit_lat_p999: metrics_dump_end.get_hit_lat_p999, + get_hit_lat_p9999: metrics_dump_end.get_hit_lat_p9999, + get_hit_lat_p99999: metrics_dump_end.get_hit_lat_p99999, + get_hit_lat_pmax: metrics_dump_end.get_hit_lat_pmax, + get_miss_lat_p50: metrics_dump_end.get_miss_lat_p50, + get_miss_lat_p90: metrics_dump_end.get_miss_lat_p90, + get_miss_lat_p99: metrics_dump_end.get_miss_lat_p99, + get_miss_lat_p999: metrics_dump_end.get_miss_lat_p999, + get_miss_lat_p9999: metrics_dump_end.get_miss_lat_p9999, + get_miss_lat_p99999: metrics_dump_end.get_miss_lat_p99999, + get_miss_lat_pmax: metrics_dump_end.get_miss_lat_pmax, + } +} + +pub async fn monitor( + iostat_path: impl AsRef, + interval: Duration, + total_secs: u64, + metrics: Metrics, + mut stop: broadcast::Receiver<()>, +) { + let mut stat = iostat(&iostat_path); + let mut metrics_dump = metrics.dump(); + + let start = Instant::now(); + + loop { + let now = Instant::now(); + match stop.try_recv() { + Err(broadcast::error::TryRecvError::Empty) => {} + _ => return, + } + + tokio::time::sleep(interval).await; + let new_stat = iostat(&iostat_path); + let new_metrics_dump = metrics.dump(); + let analysis = analyze( + // interval may have ~ +7% error + now.elapsed(), + &stat, + &new_stat, + &metrics_dump, + &new_metrics_dump, + ); + println!("[{}s/{}s]", start.elapsed().as_secs(), total_secs); + println!("{}", analysis); + stat = new_stat; + metrics_dump = new_metrics_dump; + } +} diff --git a/foyer-storage-bench/src/export.rs b/foyer-storage-bench/src/export.rs new file mode 100644 index 00000000..884aeb46 --- /dev/null +++ b/foyer-storage-bench/src/export.rs @@ -0,0 +1,68 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::net::SocketAddr; + +use foyer_storage::metrics::get_metrics_registry; +use http_body_util::Full; +use hyper::{ + body::{Body, Bytes}, + header::CONTENT_TYPE, + service::service_fn, + Request, Response, +}; +use prometheus::{Encoder, TextEncoder}; +use tokio::net::TcpListener; + +pub struct MetricsExporter; + +impl MetricsExporter { + pub fn init(addr: SocketAddr) { + tokio::spawn(async move { + tracing::info!("Prometheus service is set up on http://{}", addr); + + let listener = TcpListener::bind(addr).await.unwrap(); + loop { + let stream = match listener.accept().await { + Ok((stream, _addr)) => stream, + Err(e) => { + tracing::error!("accept conn error: {}", e); + continue; + } + }; + let io = hyper_util::rt::TokioIo::new(stream); + tokio::spawn(async move { + if let Err(e) = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()) + .serve_connection(io, service_fn(Self::serve)) + .await + { + tracing::error!("Prometheus service error: {}", e); + } + }); + } + }); + } + + async fn serve(_request: Request) -> anyhow::Result>> { + let encoder = TextEncoder::new(); + let mut buffer = Vec::with_capacity(4096); + let metrics = get_metrics_registry().gather(); + encoder.encode(&metrics, &mut buffer)?; + let response = Response::builder() + .status(200) + .header(CONTENT_TYPE, encoder.format_type()) + .body(Full::new(Bytes::from(buffer)))?; + Ok(response) + } +} diff --git a/foyer-storage-bench/src/main.rs b/foyer-storage-bench/src/main.rs new file mode 100644 index 00000000..acd59aab --- /dev/null +++ b/foyer-storage-bench/src/main.rs @@ -0,0 +1,916 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod analyze; +mod export; +mod rate; +mod text; +mod utils; + +use std::{ + collections::BTreeMap, + fs::create_dir_all, + ops::Range, + path::PathBuf, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use analyze::{analyze, monitor, Metrics}; +use clap::Parser; +use export::MetricsExporter; +use foyer_common::code::{Key, Value}; +use foyer_intrusive::eviction::lfu::LfuConfig; +use foyer_storage::{ + admission::{rated_ticket::RatedTicketAdmissionPolicy, AdmissionPolicy}, + compress::Compression, + device::fs::FsDeviceConfig, + error::Result, + reinsertion::{rated_ticket::RatedTicketReinsertionPolicy, ReinsertionPolicy}, + runtime::{RuntimeConfig, RuntimeStore, RuntimeStoreConfig, RuntimeStoreWriter}, + storage::{AsyncStorageExt, Storage, StorageExt, StorageWriter}, + store::{LfuFsStoreConfig, Store, StoreConfig, StoreWriter}, +}; +use futures::future::join_all; +use itertools::Itertools; +use rand::{ + distributions::Distribution, + rngs::{OsRng, StdRng}, + Rng, SeedableRng, +}; +use rate::RateLimiter; +use text::text; +use tokio::sync::broadcast; +use utils::{detect_fs_type, dev_stat_path, file_stat_path, iostat, FsType}; + +#[derive(Parser, Debug, Clone)] +#[command(author, version, about)] +pub struct Args { + /// dir for cache data + #[arg(short, long)] + dir: String, + + /// (MiB) + #[arg(long, default_value_t = 1024)] + capacity: usize, + + /// (s) + #[arg(short, long, default_value_t = 60)] + time: u64, + + /// (s) + #[arg(long, default_value_t = 2)] + report_interval: u64, + + /// Some filesystem (e.g. btrfs) can span across multiple block devices and it's hard to decide + /// which device to moitor. Use this argument to specify which block device to monitor. + #[arg(long, default_value = "")] + iostat_dev: String, + + /// (MiB) + #[arg(long, default_value_t = 0.0)] + w_rate: f64, + + /// (MiB) + #[arg(long, default_value_t = 0.0)] + r_rate: f64, + + #[arg(long, default_value_t = 64 * 1024)] + entry_size_min: usize, + + #[arg(long, default_value_t = 64 * 1024)] + entry_size_max: usize, + + #[arg(long, default_value_t = 10000)] + lookup_range: u64, + + /// (MiB) + #[arg(long, default_value_t = 64)] + region_size: usize, + + #[arg(long, default_value_t = 4)] + flushers: usize, + + #[arg(long, default_value_t = 4)] + reclaimers: usize, + + #[arg(long, default_value_t = 4096)] + align: usize, + + #[arg(long, default_value_t = 16 * 1024)] + io_size: usize, + + #[arg(long, default_value_t = 16)] + writers: usize, + + #[arg(long, default_value_t = 16)] + readers: usize, + + #[arg(long, default_value_t = 16)] + recover_concurrency: usize, + + /// enable rated ticket admission policy if `ticket_insert_rate_limit` > 0 + /// (MiB/s) + #[arg(long, default_value_t = 0)] + ticket_insert_rate_limit: usize, + + /// enable rated ticket reinsetion policy if `ticket_reinsert_rate_limit` > 0 + /// (MiB/s) + #[arg(long, default_value_t = 0)] + ticket_reinsert_rate_limit: usize, + + /// `0` means equal to reclaimer count + #[arg(long, default_value_t = 0)] + clean_region_threshold: usize, + + /// Catalog indices sharding bits. + #[arg(long, default_value_t = 6)] + catalog_bits: usize, + + /// weigher to enable metrics exporter + #[arg(long, default_value_t = false)] + metrics: bool, + + /// use separate runtime + #[arg(long, default_value_t = false)] + runtime: bool, + + /// available values: "none", "zstd" + #[arg(long, default_value = "none")] + compression: String, + + /// Time-series operation distribution. + /// + /// Available values: "none", "uniform", "zipf". + /// + /// If "uniform" or "zipf" is used, operations will be performed in async mode. + #[arg(long, default_value = "none")] + distribution: String, + + /// For `--distribution zipf` only. + #[arg(long, default_value_t = 100)] + distribution_zipf_n: usize, + + /// For `--distribution zipf` only. + #[arg(long, default_value_t = 0.5)] + distribution_zipf_s: f64, +} + +#[derive(Debug)] +pub enum BenchStoreConfig +where + K: Key, + V: Value, +{ + StoreConfig { config: StoreConfig }, + RuntimeStoreConfig { config: RuntimeStoreConfig }, +} + +impl Clone for BenchStoreConfig +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + match self { + Self::StoreConfig { config } => Self::StoreConfig { config: config.clone() }, + Self::RuntimeStoreConfig { config } => Self::RuntimeStoreConfig { config: config.clone() }, + } + } +} + +#[derive(Debug)] +pub enum BenchStoreWriter +where + K: Key, + V: Value, +{ + StoreWriter { writer: StoreWriter }, + RuntimeStoreWriter { writer: RuntimeStoreWriter }, +} + +impl From> for BenchStoreWriter +where + K: Key, + V: Value, +{ + fn from(writer: StoreWriter) -> Self { + Self::StoreWriter { writer } + } +} + +impl From> for BenchStoreWriter +where + K: Key, + V: Value, +{ + fn from(writer: RuntimeStoreWriter) -> Self { + Self::RuntimeStoreWriter { writer } + } +} + +impl StorageWriter for BenchStoreWriter +where + K: Key, + V: Value, +{ + type Key = K; + type Value = V; + + fn key(&self) -> &Self::Key { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.key(), + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.key(), + } + } + + fn weight(&self) -> usize { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.weight(), + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.weight(), + } + } + + fn judge(&mut self) -> bool { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.judge(), + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.judge(), + } + } + + fn force(&mut self) { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.force(), + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.force(), + } + } + + async fn finish(self, value: Self::Value) -> Result { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.finish(value).await, + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.finish(value).await, + } + } + + fn compression(&self) -> Compression { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.compression(), + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.compression(), + } + } + + fn set_compression(&mut self, compression: Compression) { + match self { + BenchStoreWriter::StoreWriter { writer } => writer.set_compression(compression), + BenchStoreWriter::RuntimeStoreWriter { writer } => writer.set_compression(compression), + } + } +} + +#[derive(Debug)] +pub enum BenchStore>> +where + K: Key, + V: Value, +{ + Store { store: Store }, + RuntimeStore { store: RuntimeStore }, +} + +impl Clone for BenchStore +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + match self { + Self::Store { store } => Self::Store { store: store.clone() }, + Self::RuntimeStore { store } => Self::RuntimeStore { store: store.clone() }, + } + } +} + +impl Storage for BenchStore +where + K: Key, + V: Value, +{ + type Key = K; + type Value = V; + type Config = BenchStoreConfig; + type Writer = BenchStoreWriter; + + async fn open(config: Self::Config) -> Result { + match config { + BenchStoreConfig::StoreConfig { config } => Store::open(config).await.map(|store| Self::Store { store }), + BenchStoreConfig::RuntimeStoreConfig { config } => RuntimeStore::open(config) + .await + .map(|store| Self::RuntimeStore { store }), + } + } + + fn is_ready(&self) -> bool { + match self { + BenchStore::Store { store } => store.is_ready(), + BenchStore::RuntimeStore { store } => store.is_ready(), + } + } + + async fn close(&self) -> Result<()> { + match self { + BenchStore::Store { store } => store.close().await, + BenchStore::RuntimeStore { store } => store.close().await, + } + } + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer { + match self { + BenchStore::Store { store } => store.writer(key, weight).into(), + BenchStore::RuntimeStore { store } => store.writer(key, weight).into(), + } + } + + fn exists(&self, key: &Self::Key) -> Result { + match self { + BenchStore::Store { store } => store.exists(key), + BenchStore::RuntimeStore { store } => store.exists(key), + } + } + + async fn lookup(&self, key: &Self::Key) -> Result> { + match self { + BenchStore::Store { store } => store.lookup(key).await, + BenchStore::RuntimeStore { store } => store.lookup(key).await, + } + } + + fn remove(&self, key: &Self::Key) -> Result { + match self { + BenchStore::Store { store } => store.remove(key), + BenchStore::RuntimeStore { store } => store.remove(key), + } + } + + fn clear(&self) -> Result<()> { + match self { + BenchStore::Store { store } => store.clear(), + BenchStore::RuntimeStore { store } => store.clear(), + } + } +} + +#[derive(Debug)] +enum TimeSeriesDistribution { + None, + Uniform { interval: Duration }, + Zipf { n: usize, s: f64, interval: Duration }, +} + +impl TimeSeriesDistribution { + fn new(args: &Args) -> Self { + match args.distribution.as_str() { + "none" => TimeSeriesDistribution::None, + "uniform" => { + // interval = 1 / freq = 1 / (rate / size) = size / rate + let interval = + ((args.entry_size_min + args.entry_size_max) >> 1) as f64 / (args.w_rate * 1024.0 * 1024.0); + let interval = Duration::from_secs_f64(interval); + TimeSeriesDistribution::Uniform { interval } + } + "zipf" => { + // interval = 1 / freq = 1 / (rate / size) = size / rate + let interval = + ((args.entry_size_min + args.entry_size_max) >> 1) as f64 / (args.w_rate * 1024.0 * 1024.0); + let interval = Duration::from_secs_f64(interval); + display_zipf_sample(args.distribution_zipf_n, args.distribution_zipf_s); + TimeSeriesDistribution::Zipf { + n: args.distribution_zipf_n, + s: args.distribution_zipf_s, + interval, + } + } + other => panic!("unsupported distribution: {}", other), + } + } +} + +struct Context { + w_rate: Option, + r_rate: Option, + counts: Vec, + entry_size_range: Range, + lookup_range: u64, + time: u64, + distribution: TimeSeriesDistribution, + metrics: Metrics, +} + +fn is_send_sync_static() {} + +#[cfg(feature = "tokio-console")] +fn init_logger() { + console_subscriber::init(); +} + +#[cfg(feature = "trace")] +fn init_logger() { + use opentelemetry_sdk::{ + trace::{BatchConfigBuilder, Config}, + Resource, + }; + use opentelemetry_semantic_conventions::resource::SERVICE_NAME; + use tracing::Level; + use tracing_subscriber::{filter::Targets, prelude::*}; + + let trace_config = Config::default().with_resource(Resource::new(vec![opentelemetry::KeyValue::new( + SERVICE_NAME, + "foyer-storage-bench", + )])); + let batch_config = BatchConfigBuilder::default() + .with_max_queue_size(1048576) + .with_max_export_batch_size(4096) + .with_max_concurrent_exports(4) + .build(); + + let tracer = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter(opentelemetry_otlp::new_exporter().tonic()) + .with_trace_config(trace_config) + .with_batch_config(batch_config) + .install_batch(opentelemetry_sdk::runtime::Tokio) + .unwrap(); + let opentelemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with( + Targets::new() + .with_target("foyer_storage", Level::DEBUG) + .with_target("foyer_common", Level::DEBUG) + .with_target("foyer_intrusive", Level::DEBUG) + .with_target("foyer_storage_bench", Level::DEBUG), + ) + .with(opentelemetry_layer) + .init(); +} + +#[cfg(not(any(feature = "tokio-console", feature = "trace")))] +fn init_logger() { + use tracing_subscriber::{prelude::*, EnvFilter}; + + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + // .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .with_line_number(true), + ) + .with(EnvFilter::from_default_env()) + .init(); +} + +#[tokio::main] +async fn main() { + is_send_sync_static::(); + + init_logger(); + + #[cfg(feature = "deadlock")] + { + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_secs(1)); + let deadlocks = parking_lot::deadlock::check_deadlock(); + if deadlocks.is_empty() { + continue; + } + + println!("{} deadlocks detected", deadlocks.len()); + for (i, threads) in deadlocks.iter().enumerate() { + println!("Deadlock #{}", i); + for t in threads { + println!("Thread Id {:#?}", t.thread_id()); + println!("{:#?}", t.backtrace()); + } + } + panic!() + }); + } + + let args = Args::parse(); + + if args.metrics { + MetricsExporter::init("0.0.0.0:19970".parse().unwrap()); + } + + println!("{:#?}", args); + + assert!(args.lookup_range > 0, "\"--lookup-range\" value must be greater than 0"); + + create_dir_all(&args.dir).unwrap(); + + let iostat_path = match detect_fs_type(&args.dir) { + FsType::Tmpfs => panic!("tmpfs is not supported with benches"), + FsType::Btrfs => { + if args.iostat_dev.is_empty() { + panic!("cannot decide which block device to monitor for btrfs, please specify device name with \'--iostat-dev\'") + } else { + dev_stat_path(&args.iostat_dev) + } + } + _ => file_stat_path(&args.dir), + }; + + let metrics = Metrics::default(); + + let iostat_start = iostat(&iostat_path); + let metrics_dump_start = metrics.dump(); + let time = Instant::now(); + + let eviction_config = LfuConfig { + window_to_cache_size_ratio: 1, + tiny_lru_capacity_ratio: 0.01, + }; + + let device_config = FsDeviceConfig { + dir: PathBuf::from(&args.dir), + capacity: args.capacity * 1024 * 1024, + file_capacity: args.region_size * 1024 * 1024, + align: args.align, + io_size: args.io_size, + }; + + let mut admissions: Vec>>>> = vec![]; + let mut reinsertions: Vec>>>> = vec![]; + if args.ticket_insert_rate_limit > 0 { + let rt = RatedTicketAdmissionPolicy::new(args.ticket_insert_rate_limit * 1024 * 1024); + admissions.push(Arc::new(rt)); + } + if args.ticket_reinsert_rate_limit > 0 { + let rt = RatedTicketReinsertionPolicy::new(args.ticket_reinsert_rate_limit * 1024 * 1024); + reinsertions.push(Arc::new(rt)); + } + + let clean_region_threshold = if args.clean_region_threshold == 0 { + args.reclaimers + } else { + args.clean_region_threshold + }; + + let compression = args + .compression + .as_str() + .try_into() + .expect("unsupported compression algorithm"); + + let config = LfuFsStoreConfig { + name: "".to_string(), + eviction_config, + device_config, + catalog_bits: args.catalog_bits, + admissions, + reinsertions, + flushers: args.flushers, + reclaimers: args.reclaimers, + recover_concurrency: args.recover_concurrency, + clean_region_threshold, + compression, + }; + + let config = if args.runtime { + BenchStoreConfig::RuntimeStoreConfig { + config: RuntimeStoreConfig { + store: config.into(), + runtime: RuntimeConfig { + worker_threads: None, + thread_name: Some("foyer".to_string()), + }, + }, + } + } else { + BenchStoreConfig::StoreConfig { config: config.into() } + }; + + println!("{config:#?}"); + + let store = BenchStore::open(config).await.unwrap(); + + let (stop_tx, _) = broadcast::channel(4096); + + let handle_monitor = tokio::spawn({ + let iostat_path = iostat_path.clone(); + let metrics = metrics.clone(); + + monitor( + iostat_path, + Duration::from_secs(args.report_interval), + args.time, + metrics, + stop_tx.subscribe(), + ) + }); + + let handle_bench = tokio::spawn(bench(args.clone(), store.clone(), metrics.clone(), stop_tx.clone())); + + let handle_signal = tokio::spawn(async move { + tokio::signal::ctrl_c().await.unwrap(); + tracing::warn!("foyer-storage-bench is cancelled with CTRL-C"); + stop_tx.send(()).unwrap(); + }); + + handle_bench.await.unwrap(); + + let iostat_end = iostat(&iostat_path); + let metrics_dump_end = metrics.dump(); + let analysis = analyze( + time.elapsed(), + &iostat_start, + &iostat_end, + &metrics_dump_start, + &metrics_dump_end, + ); + + store.close().await.unwrap(); + + handle_monitor.abort(); + handle_signal.abort(); + + println!("\nTotal:\n{}", analysis); +} + +async fn bench( + args: Args, + store: impl Storage>>, + metrics: Metrics, + stop_tx: broadcast::Sender<()>, +) { + let w_rate = if args.w_rate == 0.0 { + None + } else { + Some(args.w_rate * 1024.0 * 1024.0) + }; + let r_rate = if args.r_rate == 0.0 { + None + } else { + Some(args.r_rate * 1024.0 * 1024.0) + }; + + let counts = (0..args.writers).map(|_| AtomicU64::default()).collect_vec(); + + let distribution = TimeSeriesDistribution::new(&args); + + let context = Arc::new(Context { + w_rate, + r_rate, + lookup_range: args.lookup_range, + counts, + entry_size_range: args.entry_size_min..args.entry_size_max + 1, + time: args.time, + distribution, + metrics: metrics.clone(), + }); + + let w_handles = (0..args.writers) + .map(|id| tokio::spawn(write(id as u64, store.clone(), context.clone(), stop_tx.subscribe()))) + .collect_vec(); + let r_handles = (0..args.readers) + .map(|_| tokio::spawn(read(store.clone(), context.clone(), stop_tx.subscribe()))) + .collect_vec(); + + join_all(w_handles).await; + join_all(r_handles).await; +} + +async fn write( + id: u64, + store: impl Storage>>, + context: Arc, + mut stop: broadcast::Receiver<()>, +) { + let start = Instant::now(); + + let mut limiter = context.w_rate.map(RateLimiter::new); + let step = context.counts.len() as u64; + + const K: usize = 100; + const G: usize = 10; + + let zipf_intervals = match context.distribution { + TimeSeriesDistribution::Zipf { n, s, interval } => { + let histogram = gen_zipf_histogram(n, s, G, n * K); + + let loop_interval = Duration::from_secs_f64(interval.as_secs_f64() * K as f64); + let group_cnt = K / G; + let group_interval = interval.as_secs_f64() * group_cnt as f64; + + if id == 0 { + println!("loop interval: {loop_interval:?}, zipf intervals: "); + } + + let mut sum = 0; + let intervals = histogram + .values() + .copied() + .map(|ratio| { + let cnt = ratio * K as f64; + sum += cnt as usize; + let interval = Duration::from_secs_f64(group_interval / cnt); + if id == 0 { + println!(" [{cnt:3.0} ==> {interval:010.3?}]"); + } + (sum, interval) + }) + .collect_vec(); + + Some(intervals) + } + _ => None, + }; + + let mut c = 0; + + loop { + let l = Instant::now(); + + match stop.try_recv() { + Err(broadcast::error::TryRecvError::Empty) => {} + _ => return, + } + if start.elapsed().as_secs() >= context.time { + return; + } + + let idx = id + step * c; + let entry_size = OsRng.gen_range(context.entry_size_range.clone()); + let data = Arc::new(text(idx as usize, entry_size)); + + // TODO(MrCroxx): Use `let_chains` here after it is stable. + if let Some(limiter) = &mut limiter { + if let Some(wait) = limiter.consume(entry_size as f64) { + tokio::time::sleep(wait).await; + } + } + + let time = Instant::now(); + let ctx = context.clone(); + let callback = move |res: Result| async move { + let inserted = res.unwrap(); + let lat = time.elapsed().as_micros() as u64; + ctx.counts[id as usize].fetch_add(1, Ordering::Relaxed); + if let Err(e) = ctx.metrics.insert_lats.write().record(lat) { + tracing::error!("metrics error: {:?}, value: {}", e, lat); + } + + if inserted { + ctx.metrics.insert_ios.fetch_add(1, Ordering::Relaxed); + ctx.metrics.insert_bytes.fetch_add(entry_size, Ordering::Relaxed); + } + }; + + let elapsed = l.elapsed(); + + match &context.distribution { + TimeSeriesDistribution::None => { + let res = store.insert(idx, data).await; + callback(res).await; + } + TimeSeriesDistribution::Uniform { interval } => { + store.insert_async_with_callback(idx, data, callback); + tokio::time::sleep(interval.saturating_sub(elapsed)).await; + } + TimeSeriesDistribution::Zipf { .. } => { + store.insert_async_with_callback(idx, data, callback); + let intervals = zipf_intervals.as_ref().unwrap(); + + let group = match intervals.binary_search_by_key(&(c as usize % K), |(sum, _)| *sum) { + Ok(i) => i, + Err(i) => i.min(G - 1), + }; + + tokio::time::sleep(intervals[group].1.saturating_sub(elapsed)).await; + } + } + + c += 1; + } +} + +async fn read( + store: impl Storage>>, + context: Arc, + mut stop: broadcast::Receiver<()>, +) { + let start = Instant::now(); + + let mut limiter = context.r_rate.map(RateLimiter::new); + let step = context.counts.len() as u64; + + let mut rng = StdRng::seed_from_u64(0); + + loop { + match stop.try_recv() { + Err(broadcast::error::TryRecvError::Empty) => {} + _ => return, + } + if start.elapsed().as_secs() >= context.time { + return; + } + + let w = rng.gen_range(0..step); // pick a writer to read form + let c_max = context.counts[w as usize].load(Ordering::Relaxed); + if c_max == 0 { + tokio::time::sleep(Duration::from_millis(1)).await; + continue; + } + let c = rng.gen_range(std::cmp::max(c_max, context.lookup_range) - context.lookup_range..c_max); + let idx = w + c * step; + + let time = Instant::now(); + let res = store.lookup(&idx).await.unwrap(); + let lat = time.elapsed().as_micros() as u64; + + if let Some(buf) = res { + let entry_size = buf.len(); + assert_eq!(&text(idx as usize, entry_size), buf.as_ref()); + if let Err(e) = context.metrics.get_hit_lats.write().record(lat) { + tracing::error!("metrics error: {:?}, value: {}", e, lat); + } + context.metrics.get_bytes.fetch_add(entry_size, Ordering::Relaxed); + + // TODO(MrCroxx): Use `let_chains` here after it is stable. + if let Some(limiter) = &mut limiter { + if let Some(wait) = limiter.consume(entry_size as f64) { + tokio::time::sleep(wait).await; + } + } + } else { + if let Err(e) = context.metrics.get_miss_lats.write().record(lat) { + tracing::error!("metrics error: {:?}, value: {}", e, lat); + } + context.metrics.get_miss_ios.fetch_add(1, Ordering::Relaxed); + } + context.metrics.get_ios.fetch_add(1, Ordering::Relaxed); + + tokio::task::consume_budget().await; + } +} + +fn gen_zipf_histogram(n: usize, s: f64, groups: usize, samples: usize) -> BTreeMap { + let step = n / groups; + + let mut rng = rand::thread_rng(); + let zipf = zipf::ZipfDistribution::new(n, s).unwrap(); + let mut data: BTreeMap = BTreeMap::default(); + for _ in 0..samples { + let v = zipf.sample(&mut rng); + let g = std::cmp::min(v / step, groups); + *data.entry(g).or_default() += 1; + } + let mut histogram: BTreeMap = BTreeMap::default(); + for group in 0..groups { + histogram.insert( + group, + data.get(&group).copied().unwrap_or_default() as f64 / samples as f64, + ); + } + histogram +} + +fn display_zipf_sample(n: usize, s: f64) { + const W: usize = 100; + const H: usize = 10; + + let samples = n * 1000; + + let histogram = gen_zipf_histogram(n, s, H, samples); + + let max = histogram.values().copied().fold(0.0, f64::max); + + println!("zipf's diagram [N = {n}][s = {s}][samples = {}]", n * 1000); + + for (g, ratio) in histogram { + let shares = (ratio / max * W as f64) as usize; + let bar: String = if shares != 0 { + "=".repeat(shares) + } else { + ".".to_string() + }; + println!( + "{:3} : {:6} : {:6.3}% : {}", + g, + (samples as f64 * ratio) as usize, + ratio * 100.0, + bar + ); + } +} diff --git a/foyer-storage-bench/src/rate.rs b/foyer-storage-bench/src/rate.rs new file mode 100644 index 00000000..e7d86a34 --- /dev/null +++ b/foyer-storage-bench/src/rate.rs @@ -0,0 +1,59 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::time::{Duration, Instant}; + +pub struct RateLimiter { + capacity: f64, + quota: f64, + + last: Instant, +} + +impl RateLimiter { + pub fn new(capacity: f64) -> Self { + Self { + capacity, + quota: 0.0, + last: Instant::now(), + } + } + + pub fn consume(&mut self, weight: f64) -> Option { + let now = Instant::now(); + let refill = now.duration_since(self.last).as_secs_f64() * self.capacity; + self.last = now; + self.quota = f64::min(self.quota + refill, self.capacity); + self.quota -= weight; + if self.quota >= 0.0 { + return None; + } + let wait = Duration::from_secs_f64((-self.quota) / self.capacity); + Some(wait) + } +} diff --git a/foyer-storage-bench/src/text.rs b/foyer-storage-bench/src/text.rs new file mode 100644 index 00000000..2ec8f8f2 --- /dev/null +++ b/foyer-storage-bench/src/text.rs @@ -0,0 +1,28 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const TEXT: &[u8] = include_bytes!("../etc/sample.txt"); + +pub fn text(offset: usize, len: usize) -> Vec { + let mut res = Vec::with_capacity(len); + let mut cursor = offset % TEXT.len(); + let mut remain = len; + while remain > 0 { + let bytes = std::cmp::min(remain, TEXT.len() - cursor); + res.extend(&TEXT[cursor..cursor + bytes]); + cursor = (cursor + bytes) % TEXT.len(); + remain -= bytes; + } + res +} diff --git a/foyer-storage-bench/src/utils.rs b/foyer-storage-bench/src/utils.rs new file mode 100644 index 00000000..cf703186 --- /dev/null +++ b/foyer-storage-bench/src/utils.rs @@ -0,0 +1,170 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2023 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::path::{Path, PathBuf}; + +use itertools::Itertools; +use nix::{fcntl::readlink, sys::stat::stat}; + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum FsType { + Xfs, + Ext4, + Btrfs, + Tmpfs, + Others, +} + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] +pub fn detect_fs_type(path: impl AsRef) -> FsType { + #[cfg(target_os = "linux")] + { + use nix::sys::statfs::{statfs, BTRFS_SUPER_MAGIC, EXT4_SUPER_MAGIC, TMPFS_MAGIC, XFS_SUPER_MAGIC}; + let fs_stat = statfs(path.as_ref()).unwrap(); + match fs_stat.filesystem_type() { + XFS_SUPER_MAGIC => FsType::Xfs, + EXT4_SUPER_MAGIC => FsType::Ext4, + BTRFS_SUPER_MAGIC => FsType::Btrfs, + TMPFS_MAGIC => FsType::Tmpfs, + _ => FsType::Others, + } + } + + #[cfg(not(target_os = "linux"))] + FsType::Others +} + +/// Given a normal file path, returns the containing block device static file path (of the +/// partition). +pub fn file_stat_path(path: impl AsRef) -> PathBuf { + let st_dev = stat(path.as_ref()).unwrap().st_dev; + + let major = unsafe { libc::major(st_dev) }; + let minor = unsafe { libc::minor(st_dev) }; + + let dev = PathBuf::from("/dev/block").join(format!("{}:{}", major, minor)); + + let linkname = readlink(&dev).unwrap(); + let devname = Path::new(linkname.as_os_str()).file_name().unwrap(); + dev_stat_path(devname.to_str().unwrap()) +} + +pub fn dev_stat_path(devname: &str) -> PathBuf { + let classpath = Path::new("/sys/class/block").join(devname); + let devclass = readlink(&classpath).unwrap(); + + let devpath = Path::new(&devclass); + Path::new("/sys") + .join(devpath.strip_prefix("../..").unwrap()) + .join("stat") +} + +#[derive(Debug, Clone, Copy)] +pub struct IoStat { + /// read I/Os requests number of read I/Os processed + pub read_ios: usize, + /// read merges requests number of read I/Os merged with in-queue I/O + pub read_merges: usize, + /// read sectors sectors number of sectors read + pub read_sectors: usize, + /// read ticks milliseconds total wait time for read requests + pub read_ticks: usize, + /// write I/Os requests number of write I/Os processed + pub write_ios: usize, + /// write merges requests number of write I/Os merged with in-queue I/O + pub write_merges: usize, + /// write sectors sectors number of sectors written + pub write_sectors: usize, + /// write ticks milliseconds total wait time for write requests + pub write_ticks: usize, + /// in_flight requests number of I/Os currently in flight + pub in_flight: usize, + /// io_ticks milliseconds total time this block device has been active + pub io_ticks: usize, + /// time_in_queue milliseconds total wait time for all requests + pub time_in_queue: usize, + /// discard I/Os requests number of discard I/Os processed + pub discard_ios: usize, + /// discard merges requests number of discard I/Os merged with in-queue I/O + pub discard_merges: usize, + /// discard sectors sectors number of sectors discarded + pub discard_sectors: usize, + /// discard ticks milliseconds total wait time for discard requests + pub discard_ticks: usize, + /// flush I/Os requests number of flush I/Os processed + pub flush_ios: usize, + /// flush ticks milliseconds total wait time for flush requests + pub flush_ticks: usize, +} + +/// Given the device static file path and get the iostat. +pub fn iostat(path: impl AsRef) -> IoStat { + let content = std::fs::read_to_string(path.as_ref()).unwrap(); + let nums = content.split_ascii_whitespace().collect_vec(); + + let read_ios = nums[0].parse().unwrap(); + let read_merges = nums[1].parse().unwrap(); + let read_sectors = nums[2].parse().unwrap(); + let read_ticks = nums[3].parse().unwrap(); + let write_ios = nums[4].parse().unwrap(); + let write_merges = nums[5].parse().unwrap(); + let write_sectors = nums[6].parse().unwrap(); + let write_ticks = nums[7].parse().unwrap(); + let in_flight = nums[8].parse().unwrap(); + let io_ticks = nums[9].parse().unwrap(); + let time_in_queue = nums[10].parse().unwrap(); + let discard_ios = nums[11].parse().unwrap(); + let discard_merges = nums[12].parse().unwrap(); + let discard_sectors = nums[13].parse().unwrap(); + let discard_ticks = nums[14].parse().unwrap(); + let flush_ios = nums[15].parse().unwrap(); + let flush_ticks = nums[16].parse().unwrap(); + + IoStat { + read_ios, + read_merges, + read_sectors, + read_ticks, + write_ios, + write_merges, + write_sectors, + write_ticks, + in_flight, + io_ticks, + time_in_queue, + discard_ios, + discard_merges, + discard_sectors, + discard_ticks, + flush_ios, + flush_ticks, + } +} diff --git a/foyer-storage/Cargo.toml b/foyer-storage/Cargo.toml new file mode 100644 index 00000000..16bba14c --- /dev/null +++ b/foyer-storage/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "foyer-storage" +version = "0.6.0" +edition = "2021" +authors = ["MrCroxx "] +description = "storage engine for foyer - the hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +allocator-api2 = "0.2" # TODO(MrCroxx): Remove this after `allocator_api` is stable. +anyhow = "1.0" +bitflags = "2.3.1" +bitmaps = "3.2" +bytes = "1" +foyer-common = { version = "0.5", path = "../foyer-common" } +foyer-intrusive = { version = "0.4", path = "../foyer-intrusive" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } +futures = "0.3" +itertools = "0.12" +lazy_static = "1" +libc = "0.2" +lz4 = "1.24" +memoffset = "0.9" +nix = { version = "0.28", features = ["fs", "mman", "uio"] } +parking_lot = { version = "0.12", features = ["arc_lock"] } +paste = "1.0" +prometheus = "0.13" +rand = "0.8.5" +thiserror = "1" +tokio = { workspace = true } +tracing = "0.1" +twox-hash = "1" +zstd = "0.13" + +[dev-dependencies] +bytesize = "1" +clap = { version = "4", features = ["derive"] } +hdrhistogram = "7" +rand_mt = "4.2.1" +tempfile = "3" + +[features] +deadlock = ["parking_lot/deadlock_detection"] diff --git a/foyer-storage/src/admission/mod.rs b/foyer-storage/src/admission/mod.rs new file mode 100644 index 00000000..406ed732 --- /dev/null +++ b/foyer-storage/src/admission/mod.rs @@ -0,0 +1,57 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, sync::Arc}; + +use foyer_common::code::{Key, Value}; + +use crate::{catalog::Catalog, metrics::Metrics}; + +#[derive(Debug)] +pub struct AdmissionContext +where + K: Key, + V: Value, +{ + pub catalog: Arc>, + pub metrics: Arc, +} + +impl Clone for AdmissionContext +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + Self { + catalog: self.catalog.clone(), + metrics: self.metrics.clone(), + } + } +} + +pub trait AdmissionPolicy: Send + Sync + 'static + Debug { + type Key: Key; + type Value: Value; + + fn init(&self, context: AdmissionContext); + + fn judge(&self, key: &Self::Key, weight: usize) -> bool; + + fn on_insert(&self, key: &Self::Key, weight: usize, judge: bool); + + fn on_drop(&self, key: &Self::Key, weight: usize, judge: bool); +} + +pub mod rated_ticket; diff --git a/foyer-storage/src/admission/rated_ticket.rs b/foyer-storage/src/admission/rated_ticket.rs new file mode 100644 index 00000000..c2877c34 --- /dev/null +++ b/foyer-storage/src/admission/rated_ticket.rs @@ -0,0 +1,89 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fmt::Debug, + sync::{ + atomic::{AtomicUsize, Ordering}, + OnceLock, + }, +}; + +use foyer_common::{ + code::{Key, Value}, + rated_ticket::RatedTicket, +}; + +use super::{AdmissionContext, AdmissionPolicy}; + +#[derive(Debug)] +pub struct RatedTicketAdmissionPolicy +where + K: Key, + V: Value, +{ + inner: RatedTicket, + + last: AtomicUsize, + + context: OnceLock>, +} + +impl RatedTicketAdmissionPolicy +where + K: Key, + V: Value, +{ + pub fn new(rate: usize) -> Self { + Self { + inner: RatedTicket::new(rate as f64), + last: AtomicUsize::default(), + context: OnceLock::new(), + } + } +} + +impl AdmissionPolicy for RatedTicketAdmissionPolicy +where + K: Key, + V: Value, +{ + type Key = K; + + type Value = V; + + fn init(&self, context: AdmissionContext) { + self.context.set(context).unwrap(); + } + + fn judge(&self, _key: &Self::Key, _weight: usize) -> bool { + let res = self.inner.probe(); + + let metrics = self.context.get().unwrap().metrics.as_ref(); + let current = metrics.op_bytes_flush.get() as usize; + let last = self.last.load(Ordering::Relaxed); + let delta = current.saturating_sub(last); + + if delta > 0 { + self.last.store(current, Ordering::Relaxed); + self.inner.reduce(delta as f64); + } + + res + } + + fn on_insert(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} + + fn on_drop(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} +} diff --git a/foyer-storage/src/buffer.rs b/foyer-storage/src/buffer.rs new file mode 100644 index 00000000..5f5f2fe1 --- /dev/null +++ b/foyer-storage/src/buffer.rs @@ -0,0 +1,437 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; + +use allocator_api2::vec::Vec as VecA; +use foyer_common::{ + bits::{align_up, is_aligned}, + code::{Cursor, Key, Value}, +}; + +use crate::{ + compress::Compression, + device::{allocator::WritableVecA, error::DeviceError, Device}, + flusher::Entry, + generic::{checksum, EntryHeader}, + region::{RegionHeader, RegionId, Version, REGION_MAGIC}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum BufferError +where + R: Send + Sync + 'static + Debug, +{ + #[error("need rotate and retry {0}")] + NeedRotate(Box), + #[error("device error: {0}")] + Device(#[from] DeviceError), + #[error("other error: {0}")] + Other(#[from] anyhow::Error), +} + +pub type BufferResult = core::result::Result>; + +#[derive(Debug)] +pub struct PositionedEntry +where + K: Key, + V: Value, +{ + pub entry: Entry, + pub region: RegionId, + pub offset: usize, + pub len: usize, +} + +#[derive(Debug)] +pub struct FlushBuffer +where + K: Key, + V: Value, + D: Device, +{ + // TODO(MrCroxx): optimize buffer allocation + /// io buffer + buffer: VecA, + + /// current writing region + region: Option, + + /// current buffer offset of current writing region + offset: usize, + + /// entries in io buffer waiting for flush + entries: Vec>, + + // underlying device + device: D, + + default_buffer_capacity: usize, +} + +impl FlushBuffer +where + K: Key, + V: Value, + D: Device, +{ + pub fn new(device: D) -> Self { + let default_buffer_capacity = align_up(device.align(), device.io_size() + device.io_size() / 2); + let buffer = device.io_buffer(0, default_buffer_capacity); + Self { + buffer, + region: None, + offset: 0, + entries: vec![], + device, + default_buffer_capacity, + } + } + + pub fn region(&self) -> Option { + self.region + } + + pub fn remaining(&self) -> usize { + if self.region.is_none() { + 0 + } else { + self.device + .region_size() + .saturating_sub(self.offset + self.buffer.len()) + } + } + + /// Flush io buffer if necessary, and reset io buffer to a new region. + /// + /// Returns fully flushed entries. + pub async fn rotate(&mut self, region: RegionId) -> BufferResult>, Entry> { + let entries = self.flush().await?; + debug_assert!(self.buffer.is_empty()); + self.region = Some(region); + self.offset = 0; + + // write region header + unsafe { self.buffer.set_len(self.device.align()) }; + let header = RegionHeader { + magic: REGION_MAGIC, + version: Version::latest(), + }; + header.write(&mut self.buffer[..]); + debug_assert_eq!(self.buffer.len(), self.device.align()); + + Ok(entries) + } + + /// Flush io buffer and move the io buffer to the next position. + /// + /// The io buffer will be cleared after flush. + /// + /// Returns fully flushed entries. + pub async fn flush(&mut self) -> BufferResult>, Entry> { + let Some(region) = self.region else { + debug_assert!(self.entries.is_empty()); + return Ok(vec![]); + }; + + // align io buffer + let len = align_up(self.device.align(), self.buffer.len()); + debug_assert!(len <= self.buffer.capacity()); + unsafe { self.buffer.set_len(len) }; + debug_assert!(self.offset + self.buffer.len() <= self.device.region_size()); + + // flush and clear buffer + let mut buf = self.device.io_buffer(0, self.default_buffer_capacity); + std::mem::swap(&mut self.buffer, &mut buf); + + let (res, _buf) = self.device.write(buf, .., region, self.offset).await; + res?; + + // advance io buffer + self.offset += len; + if self.offset == self.device.region_size() { + self.region = None; + } + + let mut entries = vec![]; + std::mem::swap(&mut self.entries, &mut entries); + Ok(entries) + } + + /// Write entry to io buffer. + /// + /// The io buffer may be flushed if buffer size equals or exceeds device io size. + /// + /// Returns fully flushed entries if there is enough space in the current region. + /// Otherwise, returns `NotEnough` error with the given `entry`. + /// + /// # Format + /// + /// | header | value (compressed) | key | | + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::uninit_vec)] + pub async fn write( + &mut self, + Entry { + key, + value, + sequence, + compression, + }: Entry, + ) -> BufferResult>, Entry> { + // Notify caller to rotate buffer if there is not enough space for the entry. + // + // NOTICE: + // + // Buffer remaining size is not compared here because the compressed entry size can be + // either larger (rarely) or smaller than the uncompressed size and it can not be determined + // before compression. So we first try to compress it and rollback if it exceeds region size. + // + // P.S. About rollback, see (*). + if self.region.is_none() { + return Err(BufferError::NeedRotate(Box::new(Entry { + key, + value, + sequence, + compression, + }))); + } + + let old = self.buffer.len(); + debug_assert!(is_aligned(self.device.align(), old)); + + // reserve underlying buffer to reduce reallocation + let uncompressed = align_up( + self.device.align(), + EntryHeader::serialized_len() + key.serialized_len() + value.serialized_len(), + ); + self.buffer.reserve(old + uncompressed); + + let mut cursor = old; + // reserve space for header + cursor += EntryHeader::serialized_len(); + unsafe { self.buffer.set_len(cursor) }; + + // write value + let mut vcursor = value.into_cursor(); + match compression { + Compression::None => { + std::io::copy(&mut vcursor, &mut WritableVecA(&mut self.buffer)).map_err(DeviceError::from)?; + } + Compression::Zstd => { + zstd::stream::copy_encode(&mut vcursor, &mut WritableVecA(&mut self.buffer), 0) + .map_err(DeviceError::from)?; + } + Compression::Lz4 => { + let buf = &mut WritableVecA(&mut self.buffer); + let mut encoder = lz4::EncoderBuilder::new() + .checksum(lz4::ContentChecksum::NoChecksum) + .build(buf) + .map_err(DeviceError::from)?; + std::io::copy(&mut vcursor, &mut encoder).map_err(DeviceError::from)?; + let (_w, res) = encoder.finish(); + res.map_err(DeviceError::from)?; + } + } + let compressed_value_len = self.buffer.len() - cursor; + cursor = self.buffer.len(); + + // write key + let mut kcursor = key.into_cursor(); + std::io::copy(&mut kcursor, &mut WritableVecA(&mut self.buffer)).map_err(DeviceError::from)?; + let encoded_key_len = self.buffer.len() - cursor; + cursor = self.buffer.len(); + + // calculate checksum + cursor -= compressed_value_len + encoded_key_len; + let checksum = checksum(&self.buffer[cursor..cursor + compressed_value_len + encoded_key_len]); + + // write entry header + cursor -= EntryHeader::serialized_len(); + let header = EntryHeader { + key_len: encoded_key_len as u32, + value_len: compressed_value_len as u32, + sequence, + compression, + checksum, + }; + header.write(&mut self.buffer[cursor..cursor + EntryHeader::serialized_len()]); + + // (*) if size exceeds region limit, rollback write and return + if self.offset + self.buffer.len() > self.device.region_size() { + unsafe { self.buffer.set_len(old) }; + let key = kcursor.into_inner(); + let value = vcursor.into_inner(); + return Err(BufferError::NeedRotate(Box::new(Entry { + key, + value, + sequence, + compression, + }))); + } + + // 3. align buffer size + let target = align_up(self.device.align(), self.buffer.len()); + self.buffer.reserve(target - self.buffer.len()); + unsafe { self.buffer.set_len(target) } + + let key = kcursor.into_inner(); + let value = vcursor.into_inner(); + + self.entries.push(PositionedEntry { + entry: Entry { + key, + value, + sequence, + compression, + }, + region: self.region.unwrap(), + offset: self.offset + old, + len: self.buffer.len() - old, + }); + + // flush if buffer equals or exceeds device io size + let entries = if self.buffer.len() >= self.device.io_size() || self.remaining() == 0 { + self.flush().await? + } else { + vec![] + }; + + Ok(entries) + } +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + + use super::*; + use crate::device::fs::{FsDevice, FsDeviceConfig}; + + fn ent(size: usize) -> Entry<(), Vec> { + Entry { + key: (), + value: vec![b'x'; size], + compression: Compression::None, + sequence: 0, + } + } + + #[tokio::test] + async fn test_flush_buffer() { + let tempdir = tempdir().unwrap(); + + let device = FsDevice::open(FsDeviceConfig { + dir: tempdir.path().into(), + capacity: 256 * 1024, // 256 KiB + file_capacity: 64 * 1024, // 64 KiB + align: 4 * 1024, // 4 KiB + io_size: 16 * 1024, // 16 KiB + }) + .await + .unwrap(); + + let mut buffer = FlushBuffer::new(device.clone()); + assert_eq!(buffer.region(), None); + + const HEADER: usize = EntryHeader::serialized_len(); + + { + let entry = ent(5 * 1024 - 128); // ~ 5 KiB + + let res = buffer.write(entry).await; + let entry = match res { + Err(BufferError::NeedRotate(entry)) => *entry, + _ => panic!("should be not enough error"), + }; + + let entries = buffer.rotate(0).await.unwrap(); + assert!(entries.is_empty()); + + // 4 ~ 12 KiB + let entries = buffer.write(entry.clone()).await.unwrap(); + assert!(entries.is_empty()); + // 12 ~ 20 KiB + let entries = buffer.write(entry.clone()).await.unwrap(); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].offset, 4 * 1024); + assert_eq!(entries[1].offset, 12 * 1024); + + // 20 ~ 28 KiB + let entries = buffer.write(entry.clone()).await.unwrap(); + assert!(entries.is_empty()); + let entries = buffer.flush().await.unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].offset, 20 * 1024); + + let buf = device.io_buffer(64 * 1024, 64 * 1024); + let (res, buf) = device.read(buf, .., 0, 0).await; + res.unwrap(); + assert_eq!( + &buf[HEADER + 4 * 1024..HEADER + 9 * 1024 - 128], + &[b'x'; 5 * 1024 - 128] + ); + assert_eq!( + &buf[HEADER + 12 * 1024..HEADER + 17 * 1024 - 128], + &[b'x'; 5 * 1024 - 128] + ); + assert_eq!( + &buf[HEADER + 20 * 1024..HEADER + 25 * 1024 - 128], + &[b'x'; 5 * 1024 - 128] + ); + + assert!(buffer.entries.is_empty()); + } + + { + let entry = ent(54 * 1024 - 128); // ~ 54 KiB + + let res = buffer.write(entry).await; + let entry = match res { + Err(BufferError::NeedRotate(entry)) => *entry, + _ => panic!("should be not enough error"), + }; + + let entries = buffer.rotate(1).await.unwrap(); + assert!(entries.is_empty()); + + // 4 ~ 60 KiB + let entries = buffer.write(entry).await.unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].offset, 4 * 1024); + + let entry = ent(3 * 1024 - 128); // ~ 3 KiB + + // 60 ~ 64 KiB + let entries = buffer.write(entry).await.unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].offset, 60 * 1024); + + let buf = device.io_buffer(64 * 1024, 64 * 1024); + let (res, buf) = device.read(buf, .., 1, 0).await; + res.unwrap(); + assert_eq!( + &buf[HEADER + 4 * 1024..HEADER + 58 * 1024 - 128], + &[b'x'; 54 * 1024 - 128] + ); + assert_eq!( + &buf[HEADER + 60 * 1024..HEADER + 63 * 1024 - 128], + &[b'x'; 3 * 1024 - 128] + ); + + assert!(buffer.entries.is_empty()); + } + } +} diff --git a/foyer-storage/src/catalog.rs b/foyer-storage/src/catalog.rs new file mode 100644 index 00000000..bbc9c749 --- /dev/null +++ b/foyer-storage/src/catalog.rs @@ -0,0 +1,198 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + collections::btree_map::{BTreeMap, Entry}, + hash::Hasher, + sync::Arc, + time::Instant, +}; + +use foyer_common::code::{Key, Value}; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock}; +use twox_hash::XxHash64; + +use crate::{ + metrics::Metrics, + region::{RegionId, RegionView}, +}; + +pub type Sequence = u64; + +#[derive(Debug, Clone)] +pub enum Index +where + K: Key, + V: Value, +{ + Inflight { key: K, value: V }, + Region { view: RegionView }, +} + +#[derive(Debug, Clone)] +pub struct Item +where + K: Key, + V: Value, +{ + sequence: Sequence, + index: Index, + + inserted: Option, +} + +impl Item +where + K: Key, + V: Value, +{ + pub fn new(sequence: Sequence, index: Index) -> Self { + Self { + sequence, + index, + inserted: None, + } + } + + pub fn sequence(&self) -> &Sequence { + &self.sequence + } + + pub fn index(&self) -> &Index { + &self.index + } + + pub fn consume(self) -> (Sequence, Index) { + (self.sequence, self.index) + } +} + +#[derive(Debug)] +pub struct Catalog +where + K: Key, + V: Value, +{ + /// `items` sharding bits. + bits: usize, + + /// Sharded by key hash. + items: Vec>>>, + + /// Sharded by region id. + regions: Vec>>, + + metrics: Arc, +} + +impl Catalog +where + K: Key, + V: Value, +{ + pub fn new(regions: usize, bits: usize, metrics: Arc) -> Self { + let infos = (0..1 << bits).map(|_| RwLock::new(BTreeMap::new())).collect_vec(); + let regions = (0..regions).map(|_| Mutex::new(BTreeMap::new())).collect_vec(); + Self { + bits, + items: infos, + regions, + + metrics, + } + } + + pub fn insert(&self, key: K, mut item: Item) { + // TODO(MrCroxx): compare sequence. + + if let Index::Region { view } = &item.index { + self.regions[*view.id() as usize] + .lock() + .insert(key.clone(), item.sequence); + }; + + let shard = self.shard(&key); + // TODO(MrCroxx): handle old key? + let old = { + let mut guard = self.items[shard].write(); + item.inserted = Some(Instant::now()); + guard.insert(key.clone(), item) + }; + // TODO(MrCroxx): Use `let_chains` here after it is stable. + if let Some(old) = old { + if let Index::Inflight { .. } = old.index() { + self.metrics + .inner_op_duration_entry_flush + .observe(old.inserted.unwrap().elapsed().as_secs_f64()); + } + } + } + + pub fn lookup(&self, key: &K) -> Option> { + let shard = self.shard(key); + self.items[shard].read().get(key).cloned() + } + + pub fn remove(&self, key: &K) -> Option> { + let shard = self.shard(key); + let info: Option> = self.items[shard].write().remove(key); + // TODO(MrCroxx): Use `let_chains` here after it is stable. + if let Some(info) = &info { + if let Index::Region { view } = &info.index { + self.regions[*view.id() as usize].lock().remove(key); + } + } + info + } + + pub fn take_region(&self, region: &RegionId) -> Vec<(K, Item)> { + let mut keys = BTreeMap::new(); + std::mem::swap(&mut *self.regions[*region as usize].lock(), &mut keys); + + let mut items = Vec::with_capacity(keys.len()); + for (key, sequence) in keys { + let shard = self.shard(&key); + match self.items[shard].write().entry(key.clone()) { + Entry::Vacant(_) => continue, + Entry::Occupied(o) => { + if o.get().sequence == sequence { + let item = o.remove(); + items.push((key.clone(), item)); + } + } + }; + } + items + } + + pub fn clear(&self) { + for shard in self.items.iter() { + shard.write().clear(); + } + for region in self.regions.iter() { + region.lock().clear(); + } + } + + fn shard(&self, key: &K) -> usize { + self.hash(key) as usize & ((1 << self.bits) - 1) + } + + fn hash(&self, key: &K) -> u64 { + let mut hasher = XxHash64::default(); + key.hash(&mut hasher); + hasher.finish() + } +} diff --git a/foyer-storage/src/compress.rs b/foyer-storage/src/compress.rs new file mode 100644 index 00000000..f87c1e90 --- /dev/null +++ b/foyer-storage/src/compress.rs @@ -0,0 +1,98 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO(MrCroxx): unify compress interface? + +use anyhow::anyhow; + +const NOT_SUPPORT: &str = "compression algorithm not support"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Compression { + None, + Zstd, + Lz4, +} + +impl Compression { + pub fn to_u8(&self) -> u8 { + match self { + Self::None => 0, + Self::Zstd => 1, + Self::Lz4 => 2, + } + } + + pub fn to_str(&self) -> &str { + match self { + Self::None => "none", + Self::Zstd => "zstd", + Self::Lz4 => "lz4", + } + } +} + +impl From for u8 { + fn from(value: Compression) -> Self { + match value { + Compression::None => 0, + Compression::Zstd => 1, + Compression::Lz4 => 2, + } + } +} + +impl From for &str { + fn from(value: Compression) -> Self { + match value { + Compression::None => "none", + Compression::Zstd => "zstd", + Compression::Lz4 => "lz4", + } + } +} + +impl TryFrom for Compression { + type Error = anyhow::Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::Zstd), + 2 => Ok(Self::Lz4), + _ => Err(anyhow!(NOT_SUPPORT)), + } + } +} + +impl TryFrom<&str> for Compression { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "none" => Ok(Self::None), + "zstd" => Ok(Self::Zstd), + "lz4" => Ok(Self::Lz4), + _ => Err(anyhow!(NOT_SUPPORT)), + } + } +} + +impl TryFrom for Compression { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} diff --git a/foyer-storage/src/device/allocator.rs b/foyer-storage/src/device/allocator.rs new file mode 100644 index 00000000..81091f23 --- /dev/null +++ b/foyer-storage/src/device/allocator.rs @@ -0,0 +1,103 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use allocator_api2::{ + alloc::{AllocError, Allocator, Global}, + vec::Vec as VecA, +}; +use foyer_common::bits; + +pub struct WritableVecA<'a, T, A: Allocator>(pub &'a mut VecA); + +impl<'a, A: Allocator> std::io::Write for WritableVecA<'a, u8, A> { + #[inline] + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + + #[inline] + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result { + let len = bufs.iter().map(|b| b.len()).sum(); + self.0.reserve(len); + for buf in bufs { + self.0.extend_from_slice(buf); + } + Ok(len) + } + + // #[inline] + // fn is_write_vectored(&self) -> bool { + // true + // } + + #[inline] + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + self.0.extend_from_slice(buf); + Ok(()) + } + + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct AlignedAllocator { + align: usize, +} + +impl AlignedAllocator { + pub const fn new(align: usize) -> Self { + assert!(align.is_power_of_two()); + Self { align } + } +} + +unsafe impl Allocator for AlignedAllocator { + fn allocate(&self, layout: std::alloc::Layout) -> Result, AllocError> { + let layout = + std::alloc::Layout::from_size_align(layout.size(), bits::align_up(self.align, layout.align())).unwrap(); + Global.allocate(layout) + } + + unsafe fn deallocate(&self, ptr: std::ptr::NonNull, layout: std::alloc::Layout) { + let layout = + std::alloc::Layout::from_size_align(layout.size(), bits::align_up(self.align, layout.align())).unwrap(); + Global.deallocate(ptr, layout) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_aligned_buffer() { + const ALIGN: usize = 512; + let allocator = AlignedAllocator::new(ALIGN); + + let mut buf: VecA = VecA::with_capacity_in(ALIGN * 8, &allocator); + bits::assert_aligned(ALIGN, buf.as_ptr() as _); + + buf.extend_from_slice(&[b'x'; ALIGN * 8]); + bits::assert_aligned(ALIGN, buf.as_ptr() as _); + assert_eq!(buf, [b'x'; ALIGN * 8]); + + buf.extend_from_slice(&[b'x'; ALIGN * 8]); + bits::assert_aligned(ALIGN, buf.as_ptr() as _); + assert_eq!(buf, [b'x'; ALIGN * 16]) + } +} diff --git a/foyer-storage/src/device/error.rs b/foyer-storage/src/device/error.rs new file mode 100644 index 00000000..e9aee540 --- /dev/null +++ b/foyer-storage/src/device/error.rs @@ -0,0 +1,65 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[derive(thiserror::Error, Debug)] +#[error("{0}")] +pub struct DeviceError(Box); + +#[derive(thiserror::Error, Debug)] +#[error("{source}")] +struct DeviceErrorInner { + source: DeviceErrorKind, + // https://github.com/dtolnay/thiserror/issues/204 + // backtrace: Backtrace, +} + +#[derive(thiserror::Error, Debug)] +pub enum DeviceErrorKind { + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("nix error: {0}")] + Nix(#[from] nix::errno::Errno), + #[error("other error: {0}")] + Other(#[from] Box), +} + +impl From for DeviceError { + fn from(value: std::io::Error) -> Self { + value.into() + } +} + +impl From for DeviceError { + fn from(value: nix::errno::Errno) -> Self { + value.into() + } +} + +impl From for DeviceError { + fn from(value: String) -> Self { + value.into() + } +} + +pub type DeviceResult = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} diff --git a/foyer-storage/src/device/fs.rs b/foyer-storage/src/device/fs.rs new file mode 100644 index 00000000..06ef1eb5 --- /dev/null +++ b/foyer-storage/src/device/fs.rs @@ -0,0 +1,282 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fs::{create_dir_all, File, OpenOptions}, + os::fd::{AsRawFd, BorrowedFd, RawFd}, + path::PathBuf, + sync::Arc, +}; + +use allocator_api2::vec::Vec as VecA; +use foyer_common::range::RangeBoundsExt; +use futures::future::try_join_all; +use itertools::Itertools; + +use super::{ + allocator::AlignedAllocator, + asyncify, + error::{DeviceError, DeviceResult}, + Device, IoBuf, IoBufMut, IoRange, +}; +use crate::region::RegionId; + +#[derive(Debug, Clone)] +pub struct FsDeviceConfig { + /// base dir path + pub dir: PathBuf, + + /// must be multipliers of `align` and `file_capacity` + pub capacity: usize, + + /// must be multipliers of `align` + pub file_capacity: usize, + + /// io block alignment, must be pow of 2 + pub align: usize, + + /// recommended optimized io block size + pub io_size: usize, +} + +impl FsDeviceConfig { + pub fn verify(&self) { + assert!(self.align.is_power_of_two()); + assert_eq!(self.file_capacity % self.align, 0); + assert_eq!(self.capacity % self.file_capacity, 0); + } +} + +#[derive(Debug)] +struct FsDeviceInner { + config: FsDeviceConfig, + + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + dir: File, + + files: Vec, + + io_buffer_allocator: AlignedAllocator, +} + +#[derive(Debug, Clone)] +pub struct FsDevice { + inner: Arc, +} + +impl Device for FsDevice { + type Config = FsDeviceConfig; + type IoBufferAllocator = AlignedAllocator; + + async fn open(config: FsDeviceConfig) -> DeviceResult { + Self::open(config).await + } + + async fn write(&self, buf: B, range: impl IoRange, region: RegionId, offset: usize) -> (DeviceResult, B) + where + B: IoBuf, + { + let file_capacity = self.inner.config.file_capacity; + + let range = range.bounds(0..buf.as_ref().len()); + let len = RangeBoundsExt::size(&range).unwrap(); + + assert!( + offset + len <= file_capacity, + "offset ({offset}) + len ({len}) <= file capacity ({file_capacity})" + ); + + let fd = self.fd(region); + + asyncify(move || { + let fd = unsafe { BorrowedFd::borrow_raw(fd) }; + let res = nix::sys::uio::pwrite(fd, &buf.as_ref()[range], offset as i64).map_err(DeviceError::from); + (res, buf) + }) + .await + } + + async fn read( + &self, + mut buf: B, + range: impl IoRange, + region: RegionId, + offset: usize, + ) -> (DeviceResult, B) + where + B: IoBufMut, + { + let file_capacity = self.inner.config.file_capacity; + + let range = range.bounds(0..buf.as_ref().len()); + let len = RangeBoundsExt::size(&range).unwrap(); + + assert!( + offset + len <= file_capacity, + "offset ({offset}) + len ({len}) <= file capacity ({file_capacity})" + ); + + let fd = self.fd(region); + + asyncify(move || { + let fd = unsafe { BorrowedFd::borrow_raw(fd) }; + let res = nix::sys::uio::pread(fd, &mut buf.as_mut()[range], offset as i64).map_err(DeviceError::from); + (res, buf) + }) + .await + } + + #[cfg(target_os = "linux")] + async fn flush(&self) -> DeviceResult<()> { + let fd = self.inner.dir.as_raw_fd(); + // Commit fs cache to disk. Linux waits for I/O completions. + // + // See also [syncfs(2)](https://man7.org/linux/man-pages/man2/sync.2.html) + asyncify(move || nix::unistd::syncfs(fd).map_err(DeviceError::from)).await?; + Ok(()) + } + + #[cfg(not(target_os = "linux"))] + async fn flush(&self) -> DeviceResult<()> { + // TODO(MrCroxx): track dirty files and call fsync(2) on them on other target os. + Ok(()) + } + + fn capacity(&self) -> usize { + self.inner.config.capacity + } + + fn regions(&self) -> usize { + self.inner.files.len() + } + + fn align(&self) -> usize { + self.inner.config.align + } + + fn io_size(&self) -> usize { + self.inner.config.io_size + } + + fn io_buffer_allocator(&self) -> &Self::IoBufferAllocator { + &self.inner.io_buffer_allocator + } + + fn io_buffer(&self, len: usize, capacity: usize) -> VecA { + assert!(len <= capacity); + let mut buf = VecA::with_capacity_in(capacity, self.inner.io_buffer_allocator); + unsafe { buf.set_len(len) }; + buf + } +} + +impl FsDevice { + pub async fn open(config: FsDeviceConfig) -> DeviceResult { + config.verify(); + + // TODO(MrCroxx): write and read config to a manifest file for pinning + + let regions = config.capacity / config.file_capacity; + + let path = config.dir.clone(); + let dir = asyncify(move || { + create_dir_all(&path)?; + File::open(&path).map_err(DeviceError::from) + }) + .await?; + + let futures = (0..regions) + .map(|i| { + let path = config.dir.clone().join(Self::filename(i as RegionId)); + async move { + #[cfg(target_os = "linux")] + use std::os::unix::prelude::OpenOptionsExt; + + let mut opts = OpenOptions::new(); + opts.create(true); + opts.write(true); + opts.read(true); + #[cfg(target_os = "linux")] + opts.custom_flags(libc::O_DIRECT); + + let file = opts.open(path)?; + + Ok::<_, DeviceError>(file) + } + }) + .collect_vec(); + let files = try_join_all(futures).await?; + + let io_buffer_allocator = AlignedAllocator::new(config.align); + + let inner = FsDeviceInner { + config, + dir, + files, + io_buffer_allocator, + }; + + Ok(Self { inner: Arc::new(inner) }) + } + + fn fd(&self, region: RegionId) -> RawFd { + self.inner.files[region as usize].as_raw_fd() + } + + fn filename(region: RegionId) -> String { + format!("foyer-cache-{:08}", region) + } +} + +#[cfg(test)] +mod tests { + + use bytes::BufMut; + + use super::*; + + const FILES: usize = 8; + const FILE_CAPACITY: usize = 8 * 1024; // 8 KiB + const CAPACITY: usize = FILES * FILE_CAPACITY; // 64 KiB + const ALIGN: usize = 4 * 1024; + + #[tokio::test] + async fn test_fs_device_simple() { + let dir = tempfile::tempdir().unwrap(); + let config = FsDeviceConfig { + dir: PathBuf::from(dir.path()), + capacity: CAPACITY, + file_capacity: FILE_CAPACITY, + align: ALIGN, + io_size: ALIGN, + }; + let dev = FsDevice::open(config).await.unwrap(); + + let mut wbuffer = dev.io_buffer(ALIGN, ALIGN); + (&mut wbuffer[..]).put_slice(&[b'x'; ALIGN]); + let mut rbuffer = dev.io_buffer(ALIGN, ALIGN); + (&mut rbuffer[..]).put_slice(&[0; ALIGN]); + + let (res, wbuffer) = dev.write(wbuffer, .., 0, 0).await; + res.unwrap(); + let (res, rbuffer) = dev.read(rbuffer, .., 0, 0).await; + res.unwrap(); + + assert_eq!(&wbuffer, &rbuffer); + + drop(wbuffer); + drop(rbuffer); + } +} diff --git a/foyer-storage/src/device/mod.rs b/foyer-storage/src/device/mod.rs new file mode 100644 index 00000000..2e35ccfc --- /dev/null +++ b/foyer-storage/src/device/mod.rs @@ -0,0 +1,228 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod allocator; +pub mod error; +pub mod fs; + +use std::fmt::Debug; + +use allocator_api2::{alloc::Allocator, vec::Vec as VecA}; +use error::DeviceResult; +use foyer_common::range::RangeBoundsExt; +use futures::Future; + +use crate::region::RegionId; + +// TODO(MrCroxx): Use `trait_alias` after stable. + +// pub trait BufferAllocator = Allocator + Clone + Send + Sync + 'static + Debug; +// pub trait IoBuf = AsRef<[u8]> + Send + Sync + 'static + Debug; +// pub trait IoBufMut = AsRef<[u8]> + AsMut<[u8]> + Send + Sync + 'static + Debug; +// pub trait IoRange = RangeBoundsExt + Sized + Send + Sync + 'static + Debug; + +pub trait BufferAllocator: Allocator + Clone + Send + Sync + 'static + Debug {} +impl BufferAllocator for T {} +pub trait IoBuf: AsRef<[u8]> + Send + Sync + 'static + Debug {} +impl + Send + Sync + 'static + Debug> IoBuf for T {} +pub trait IoBufMut: AsRef<[u8]> + AsMut<[u8]> + Send + Sync + 'static + Debug {} +impl + AsMut<[u8]> + Send + Sync + 'static + Debug> IoBufMut for T {} +pub trait IoRange: RangeBoundsExt + Sized + Send + Sync + 'static + Debug {} +impl + Sized + Send + Sync + 'static + Debug> IoRange for T {} + +pub trait Device: Sized + Clone + Send + Sync + 'static + Debug { + type IoBufferAllocator: BufferAllocator; + type Config: Send + Debug + Clone; + + #[must_use] + fn open(config: Self::Config) -> impl Future> + Send; + + #[must_use] + fn write( + &self, + buf: B, + range: impl IoRange, + region: RegionId, + offset: usize, + ) -> impl Future, B)> + Send + where + B: IoBuf; + + #[must_use] + fn read( + &self, + buf: B, + range: impl IoRange, + region: RegionId, + offset: usize, + ) -> impl Future, B)> + Send + where + B: IoBufMut; + + #[must_use] + fn flush(&self) -> impl Future> + Send; + + fn capacity(&self) -> usize; + + fn regions(&self) -> usize; + + /// must be power of 2 + fn align(&self) -> usize; + + /// optimized io size + fn io_size(&self) -> usize; + + fn io_buffer_allocator(&self) -> &Self::IoBufferAllocator; + + fn io_buffer(&self, len: usize, capacity: usize) -> VecA; + + fn region_size(&self) -> usize { + debug_assert!(self.capacity() % self.regions() == 0); + self.capacity() / self.regions() + } +} + +pub trait DeviceExt: Device { + #[must_use] + fn load( + &self, + region: RegionId, + range: impl IoRange, + ) -> impl Future>> + Send { + async move { + let range = range.bounds(0..self.region_size()); + let size = range.size().unwrap(); + debug_assert_eq!(size & (self.align() - 1), 0); + + let mut buf = self.io_buffer(size, size); + let mut offset = 0; + + while range.start + offset < range.end { + let len = std::cmp::min(self.io_size(), size - offset); + let (res, b) = self.read(buf, offset..offset + len, region, range.start + offset).await; + let bytes = res?; + offset += bytes; + buf = b; + if bytes != len { + break; + } + } + + unsafe { buf.set_len(offset) }; + + Ok(buf) + } + } +} + +impl DeviceExt for D {} + +#[cfg(not(madsim))] +#[tracing::instrument(level = "trace", skip(f))] +async fn asyncify(f: F) -> T +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + tokio::task::spawn_blocking(f).await.unwrap() +} + +#[cfg(madsim)] +#[tracing::instrument(level = "trace", skip(f))] +async fn asyncify(f: F) -> T +where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, +{ + f() +} + +#[cfg(test)] +pub mod tests { + use super::{allocator::AlignedAllocator, *}; + + #[derive(Debug, Clone)] + pub struct NullDevice(AlignedAllocator); + + impl NullDevice { + pub fn new(align: usize) -> Self { + Self(AlignedAllocator::new(align)) + } + } + + impl Device for NullDevice { + type Config = usize; + type IoBufferAllocator = AlignedAllocator; + + async fn open(config: usize) -> DeviceResult { + Ok(Self::new(config)) + } + + async fn write( + &self, + buf: B, + _range: impl IoRange, + _region: RegionId, + _offset: usize, + ) -> (DeviceResult, B) + where + B: IoBuf, + { + (Ok(0), buf) + } + + async fn read( + &self, + buf: B, + _range: impl IoRange, + _region: RegionId, + _offset: usize, + ) -> (DeviceResult, B) + where + B: IoBufMut, + { + (Ok(0), buf) + } + + async fn flush(&self) -> DeviceResult<()> { + Ok(()) + } + + fn capacity(&self) -> usize { + usize::MAX + } + + fn regions(&self) -> usize { + 4096 + } + + fn align(&self) -> usize { + 4096 + } + + fn io_size(&self) -> usize { + 4096 + } + + fn io_buffer_allocator(&self) -> &Self::IoBufferAllocator { + &self.0 + } + + fn io_buffer(&self, len: usize, capacity: usize) -> VecA { + let mut buf = VecA::with_capacity_in(capacity, self.0); + unsafe { buf.set_len(len) }; + buf + } + } +} diff --git a/foyer-storage/src/error.rs b/foyer-storage/src/error.rs new file mode 100644 index 00000000..55c9e254 --- /dev/null +++ b/foyer-storage/src/error.rs @@ -0,0 +1,80 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{buffer::BufferError, device::error::DeviceError}; + +#[derive(thiserror::Error, Debug)] +#[error("{0}")] +pub struct Error(Box); + +#[derive(thiserror::Error, Debug)] +#[error("{source}")] +struct ErrorInner { + source: ErrorKind, + // https://github.com/dtolnay/thiserror/issues/204 + // backtrace: Backtrace, +} + +#[derive(thiserror::Error, Debug)] +pub enum ErrorKind { + #[error("device error: {0}")] + Device(#[from] DeviceError), + #[error("buffer error: {0}")] + Buffer(anyhow::Error), + #[error("other error: {0}")] + Other(#[from] anyhow::Error), +} + +impl From for Error { + fn from(value: ErrorKind) -> Self { + value.into() + } +} + +impl From for Error { + fn from(value: DeviceError) -> Self { + value.into() + } +} + +impl From> for Error +where + R: Send + Sync + 'static + std::fmt::Debug, +{ + fn from(value: BufferError) -> Self { + match value { + BufferError::NeedRotate(_) => panic!("BufferError::NeedRotate should not be raised!"), + BufferError::Device(e) => From::from(e), + BufferError::Other(e) => From::from(e), + } + } +} + +impl From for Error { + fn from(value: anyhow::Error) -> Self { + value.into() + } +} + +pub type Result = core::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} diff --git a/foyer-storage/src/flusher.rs b/foyer-storage/src/flusher.rs new file mode 100644 index 00000000..84697268 --- /dev/null +++ b/foyer-storage/src/flusher.rs @@ -0,0 +1,217 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, sync::Arc}; + +use foyer_common::code::{Key, Value}; +use foyer_intrusive::{core::adapter::Link, eviction::EvictionPolicy}; +use tokio::sync::{broadcast, mpsc}; +use tracing::Instrument; + +use crate::{ + buffer::{BufferError, FlushBuffer, PositionedEntry}, + catalog::{Catalog, Index, Item, Sequence}, + compress::Compression, + device::Device, + error::Result, + metrics::Metrics, + region_manager::{RegionEpItemAdapter, RegionManager}, +}; + +pub struct Entry +where + K: Key, + V: Value, +{ + pub key: K, + pub value: V, + pub sequence: Sequence, + pub compression: Compression, +} + +impl Debug for Entry +where + K: Key, + V: Value, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Entry") + .field("sequence", &self.sequence) + .field("compression", &self.compression) + .finish() + } +} + +impl Clone for Entry +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + Self { + key: self.key.clone(), + value: self.value.clone(), + sequence: self.sequence, + compression: self.compression, + } + } +} + +#[derive(Debug)] +pub struct Flusher +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + region_manager: Arc>, + + catalog: Arc>, + + buffer: FlushBuffer, + + entry_rx: mpsc::UnboundedReceiver>, + + metrics: Arc, + + stop_rx: broadcast::Receiver<()>, +} + +impl Flusher +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + pub fn new( + region_manager: Arc>, + catalog: Arc>, + device: D, + entry_rx: mpsc::UnboundedReceiver>, + metrics: Arc, + stop_rx: broadcast::Receiver<()>, + ) -> Self { + let buffer = FlushBuffer::new(device.clone()); + Self { + region_manager, + catalog, + buffer, + entry_rx, + metrics, + stop_rx, + } + } + + pub async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + biased; + entry = self.entry_rx.recv() => { + let Some(entry) = entry else { + self.buffer.flush().await?; + tracing::info!("[flusher] exit"); + return Ok(()); + }; + self.handle(entry).await?; + } + _ = self.stop_rx.recv() => { + self.buffer.flush().await?; + tracing::info!("[flusher] exit"); + return Ok(()) + } + } + } + } + + async fn handle(&mut self, entry: Entry) -> Result<()> { + let timer = self.metrics.inner_op_duration_flusher_handle.start_timer(); + + let old_region = self.buffer.region(); + + let entry = match self.buffer.write(entry).await { + Err(BufferError::NeedRotate(entry)) => *entry, + Ok(entries) => return self.update_catalog(entries).await, + Err(e) => return Err(e.into()), + }; + + // current region is full, rotate flush buffer region and retry + + // 1. get a clean region + let acquire_clean_region_timer = self.metrics.inner_op_duration_acquire_clean_region.start_timer(); + let new_region = self + .region_manager + .clean_regions() + .acquire() + .instrument(tracing::debug_span!("acquire_clean_region")) + .await; + drop(acquire_clean_region_timer); + + // 2. rotate flush buffer + let entries = self.buffer.rotate(new_region).await?; + self.update_catalog(entries).await?; + if let Some(old_region) = old_region { + self.region_manager.eviction_push(old_region); + } + + self.metrics + .total_bytes + .add(self.region_manager.region(&new_region).device().region_size() as u64); + + // 3. retry write + let entries = match self.buffer.write(entry).await { + Err(BufferError::NeedRotate(_)) => unreachable!(), + result => result?, + }; + + self.update_catalog(entries).await?; + + drop(timer); + Ok(()) + } + + #[tracing::instrument(skip(self))] + async fn update_catalog(&self, entries: Vec>) -> Result<()> { + if entries.is_empty() { + return Ok(()); + } + + // record fully flushed bytes by the way + let mut bytes = 0; + + let timer = self.metrics.inner_op_duration_update_catalog.start_timer(); + for PositionedEntry { + entry: Entry { key, sequence, .. }, + region, + offset, + len, + } in entries + { + bytes += len; + let index = Index::Region { + view: self.region_manager.region(®ion).view(offset as u32, len as u32), + }; + let item = Item::new(sequence, index); + self.catalog.insert(key, item); + } + drop(timer); + + self.metrics.op_bytes_flush.inc_by(bytes as u64); + + Ok(()) + } +} diff --git a/foyer-storage/src/generic.rs b/foyer-storage/src/generic.rs new file mode 100644 index 00000000..51522814 --- /dev/null +++ b/foyer-storage/src/generic.rs @@ -0,0 +1,1176 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fmt::Debug, + hash::Hasher, + marker::PhantomData, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use anyhow::anyhow; +use bitmaps::Bitmap; +use bytes::{Buf, BufMut}; +use foyer_common::{ + bits, + code::{CodingError, Key, Value}, +}; +use foyer_intrusive::{core::adapter::Link, eviction::EvictionPolicy}; +use futures::future::try_join_all; +use itertools::Itertools; +use parking_lot::Mutex; +use tokio::{ + sync::{broadcast, mpsc, Semaphore}, + task::JoinHandle, +}; +use twox_hash::XxHash64; + +use crate::{ + admission::{AdmissionContext, AdmissionPolicy}, + catalog::{Catalog, Index, Item, Sequence}, + compress::Compression, + device::Device, + error::Result, + flusher::{Entry, Flusher}, + judge::Judges, + metrics::{Metrics, METRICS}, + reclaimer::Reclaimer, + region::{Region, RegionHeader, RegionId}, + region_manager::{RegionEpItemAdapter, RegionManager}, + reinsertion::{ReinsertionContext, ReinsertionPolicy}, + storage::{Storage, StorageWriter}, +}; + +const DEFAULT_BROADCAST_CAPACITY: usize = 4096; + +pub struct GenericStoreConfig +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy, +{ + /// For distinguish different foyer metrics. + /// + /// Metrics of this foyer instance has label `foyer = {{ name }}`. + pub name: String, + + /// Evictino policy configurations. + pub eviction_config: EP::Config, + + /// Device configurations. + pub device_config: D::Config, + + /// Catalog indices sharding bits. + pub catalog_bits: usize, + + /// Admission policies. + pub admissions: Vec>>, + + /// Reinsertion policies. + pub reinsertions: Vec>>, + + /// Count of flushers. + pub flushers: usize, + + /// Count of reclaimers. + pub reclaimers: usize, + + /// Clean region count threshold to trigger reclamation. + /// + /// `clean_region_threshold` is recommended to be equal or larger than `reclaimers`. + pub clean_region_threshold: usize, + + /// Concurrency of recovery. + pub recover_concurrency: usize, + + /// Compression algorithm. + pub compression: Compression, +} + +impl Debug for GenericStoreConfig +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StoreConfig") + .field("eviction_config", &self.eviction_config) + .field("device_config", &self.device_config) + .field("catalog_bits", &self.catalog_bits) + .field("admissions", &self.admissions) + .field("reinsertions", &self.reinsertions) + .field("flushers", &self.flushers) + .field("reclaimers", &self.reclaimers) + .field("clean_region_threshold", &self.clean_region_threshold) + .field("recover_concurrency", &self.recover_concurrency) + .field("compression", &self.compression) + .finish() + } +} + +impl Clone for GenericStoreConfig +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy, +{ + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + eviction_config: self.eviction_config.clone(), + device_config: self.device_config.clone(), + catalog_bits: self.catalog_bits, + admissions: self.admissions.clone(), + reinsertions: self.reinsertions.clone(), + flushers: self.flushers, + reclaimers: self.reclaimers, + clean_region_threshold: self.clean_region_threshold, + recover_concurrency: self.recover_concurrency, + compression: self.compression, + } + } +} + +#[derive(Debug)] +pub struct GenericStore +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + inner: Arc>, +} + +impl Clone for GenericStore +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +#[derive(Debug)] +pub struct GenericStoreInner +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + sequence: AtomicU64, + catalog: Arc>, + + region_manager: Arc>, + + device: D, + + admissions: Vec>>, + reinsertions: Vec>>, + + flusher_entry_txs: Vec>>, + flusher_handles: Mutex>>, + flushers_stop_tx: broadcast::Sender<()>, + + reclaimer_handles: Mutex>>, + reclaimers_stop_tx: broadcast::Sender<()>, + + metrics: Arc, + + compression: Compression, + + _marker: PhantomData, +} + +impl GenericStore +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + async fn open(config: GenericStoreConfig) -> Result { + tracing::info!("open store with config:\n{:#?}", config); + + let metrics = Arc::new(METRICS.foyer(&config.name)); + + let device = D::open(config.device_config).await?; + assert!(device.regions() >= config.flushers * 2); + + let region_manager = Arc::new(RegionManager::new( + device.regions(), + config.eviction_config, + device.clone(), + )); + + let catalog = Arc::new(Catalog::new(device.regions(), config.catalog_bits, metrics.clone())); + + let (flushers_stop_tx, _) = broadcast::channel(DEFAULT_BROADCAST_CAPACITY); + let flusher_stop_rxs = (0..config.flushers).map(|_| flushers_stop_tx.subscribe()).collect_vec(); + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::type_complexity)] + let (flusher_entry_txs, flusher_entry_rxs): ( + Vec>>, + Vec>>, + ) = (0..config.flushers).map(|_| mpsc::unbounded_channel()).unzip(); + + let (reclaimers_stop_tx, _) = broadcast::channel(DEFAULT_BROADCAST_CAPACITY); + let reclaimer_stop_rxs = (0..config.reclaimers) + .map(|_| reclaimers_stop_tx.subscribe()) + .collect_vec(); + + let inner = GenericStoreInner { + sequence: AtomicU64::new(0), + catalog: catalog.clone(), + region_manager: region_manager.clone(), + device: device.clone(), + admissions: config.admissions, + reinsertions: config.reinsertions, + flusher_entry_txs, + flusher_handles: Mutex::new(vec![]), + reclaimer_handles: Mutex::new(vec![]), + flushers_stop_tx, + reclaimers_stop_tx, + metrics: metrics.clone(), + compression: config.compression, + _marker: PhantomData, + }; + let store = Self { inner: Arc::new(inner) }; + + let admission_context = AdmissionContext { + catalog: catalog.clone(), + metrics: metrics.clone(), + }; + let reinsertion_context = ReinsertionContext { + catalog: catalog.clone(), + metrics: metrics.clone(), + }; + + for admission in store.inner.admissions.iter() { + admission.init(admission_context.clone()); + } + for reinsertion in store.inner.reinsertions.iter() { + reinsertion.init(reinsertion_context.clone()); + } + + let flushers = flusher_stop_rxs + .into_iter() + .zip_eq(flusher_entry_rxs.into_iter()) + .map(|(stop_rx, entry_rx)| { + Flusher::new( + region_manager.clone(), + catalog.clone(), + device.clone(), + entry_rx, + metrics.clone(), + stop_rx, + ) + }) + .collect_vec(); + + let reclaimers = reclaimer_stop_rxs + .into_iter() + .map(|stop_rx| { + Reclaimer::new( + config.clean_region_threshold, + store.clone(), + region_manager.clone(), + metrics.clone(), + stop_rx, + ) + }) + .collect_vec(); + + let sequence = store.recover(config.recover_concurrency).await?; + store.inner.sequence.store(sequence + 1, Ordering::Relaxed); + + let flusher_handles = flushers + .into_iter() + .map(|flusher| tokio::spawn(async move { flusher.run().await.unwrap() })) + .collect_vec(); + let reclaimer_handles = reclaimers + .into_iter() + .map(|reclaimer| tokio::spawn(async move { reclaimer.run().await.unwrap() })) + .collect_vec(); + + *store.inner.flusher_handles.lock() = flusher_handles; + *store.inner.reclaimer_handles.lock() = reclaimer_handles; + + Ok(store) + } + + async fn close(&self) -> Result<()> { + // stop and wait for flushers + let handles = self.inner.flusher_handles.lock().drain(..).collect_vec(); + if !handles.is_empty() { + self.inner.flushers_stop_tx.send(()).unwrap(); + } + for handle in handles { + handle.await.unwrap(); + } + + // stop and wait for reclaimers + let handles = self.inner.reclaimer_handles.lock().drain(..).collect_vec(); + if !handles.is_empty() { + self.inner.reclaimers_stop_tx.send(()).unwrap(); + } + for handle in handles { + handle.await.unwrap(); + } + + Ok(()) + } + + /// `weight` MUST be equal to `key.serialized_len() + value.serialized_len()` + #[tracing::instrument(skip(self))] + fn writer(&self, key: K, weight: usize) -> GenericStoreWriter { + GenericStoreWriter::new(self.clone(), key, weight) + } + + #[tracing::instrument(skip(self))] + fn exists(&self, key: &K) -> Result { + Ok(self.inner.catalog.lookup(key).is_some()) + } + + #[tracing::instrument(skip(self))] + async fn lookup(&self, key: &K) -> Result> { + let now = Instant::now(); + + let (_sequence, index) = match self.inner.catalog.lookup(key) { + Some(item) => item.consume(), + None => { + self.inner + .metrics + .op_duration_lookup_miss + .observe(now.elapsed().as_secs_f64()); + return Ok(None); + } + }; + + match index { + crate::catalog::Index::Inflight { key: _, value } => { + let value = value.clone(); + + self.inner + .metrics + .op_duration_lookup_hit + .observe(now.elapsed().as_secs_f64()); + + Ok(Some(value)) + } + crate::catalog::Index::Region { view } => { + let region = view.id(); + + self.inner.region_manager.record_access(region); + let region = self.inner.region_manager.region(region); + + // TODO(MrCroxx): read value only + let buf = match region.load(view).await? { + Some(buf) => buf, + None => { + // Remove index if the storage layer fails to lookup it (because of region version mismatch). + self.inner.catalog.remove(key); + self.inner + .metrics + .op_duration_lookup_miss + .observe(now.elapsed().as_secs_f64()); + return Ok(None); + } + }; + + let res = match read_entry::(buf.as_ref()) { + Ok((_key, value)) => { + self.inner.metrics.op_bytes_lookup.inc_by(value.serialized_len() as u64); + Ok(Some(value)) + } + Err(e) => { + // Remove index if the storage layer fails to lookup it (because of entry magic mismatch). + self.inner.catalog.remove(key); + Err(e) + } + }; + + self.inner + .metrics + .op_duration_lookup_hit + .observe(now.elapsed().as_secs_f64()); + + res + } + } + } + + #[tracing::instrument(skip(self))] + fn remove(&self, key: &K) -> Result { + let _timer = self.inner.metrics.op_duration_remove.start_timer(); + + let res = self.inner.catalog.remove(key).is_some(); + + Ok(res) + } + + #[tracing::instrument(skip(self))] + fn clear(&self) -> Result<()> { + self.inner.catalog.clear(); + + // TODO(MrCroxx): set all regions as clean? + + Ok(()) + } + + pub(crate) fn catalog(&self) -> &Arc> { + &self.inner.catalog + } + + pub(crate) fn reinsertions(&self) -> &Vec>> { + &self.inner.reinsertions + } + + #[tracing::instrument(skip(self))] + async fn recover(&self, concurrency: usize) -> Result { + tracing::info!("start store recovery"); + + let semaphore = Arc::new(Semaphore::new(concurrency)); + + let mut handles = vec![]; + for region_id in 0..self.inner.device.regions() as RegionId { + let semaphore = semaphore.clone(); + let region_manager = self.inner.region_manager.clone(); + let indices = self.inner.catalog.clone(); + let handle = tokio::spawn(async move { + let permit = semaphore.acquire().await; + let res = Self::recover_region(region_id, region_manager, indices).await; + drop(permit); + res + }); + handles.push(handle); + } + + let mut recovered = 0; + let mut sequence = 0; + + let results = try_join_all(handles).await.map_err(anyhow::Error::from)?; + + for (region_id, result) in results.into_iter().enumerate() { + if let Some(seq) = result? { + tracing::debug!("region {} is recovered", region_id); + recovered += 1; + sequence = std::cmp::max(sequence, seq); + } + } + + tracing::info!("finish store recovery, {} region recovered", recovered); + self.inner + .metrics + .total_bytes + .set((recovered * self.inner.device.region_size()) as u64); + + // Force trigger reclamation. + if recovered == self.inner.device.regions() { + self.inner.region_manager.clean_regions().flash(); + } + + Ok(sequence) + } + + /// Return `Some(max sequence)` if region is valid, otherwise `None` + async fn recover_region( + region_id: RegionId, + region_manager: Arc>, + catalog: Arc>, + ) -> Result> { + let region = region_manager.region(®ion_id).clone(); + let mut sequence = 0; + let res = if let Some(mut iter) = RegionEntryIter::::open(region).await? { + while let Some((key, item)) = iter.next().await? { + sequence = std::cmp::max(sequence, *item.sequence()); + catalog.insert(key, item); + } + region_manager.eviction_push(region_id); + Some(sequence) + } else { + region_manager.clean_regions().release(region_id); + None + }; + Ok(res) + } + + fn judge_inner(&self, writer: &mut GenericStoreWriter) { + for (index, admission) in self.inner.admissions.iter().enumerate() { + let judge = admission.judge(writer.key.as_ref().unwrap(), writer.weight); + writer.judges.set(index, judge); + } + writer.is_judged = true; + } + + #[tracing::instrument(skip(self, value))] + async fn apply_writer(&self, mut writer: GenericStoreWriter, value: V) -> Result { + debug_assert!(!writer.is_inserted); + + if !writer.judge() { + return Ok(false); + } + + let now = Instant::now(); + + let sequence = if let Some(sequence) = writer.sequence { + sequence + } else { + self.inner.sequence.fetch_add(1, Ordering::Relaxed) + }; + + writer.is_inserted = true; + let key = writer.key.take().unwrap(); + + for (i, admission) in self.inner.admissions.iter().enumerate() { + let judge = writer.judges.get(i); + admission.on_insert(&key, writer.weight, judge); + } + + // record aligned header + key + value size for metrics + let len = bits::align_up( + self.inner.device.align(), + EntryHeader::serialized_len() + key.serialized_len() + value.serialized_len(), + ); + self.inner.metrics.op_bytes_insert.inc_by(len as u64); + self.inner.metrics.insert_entry_bytes.observe(len as f64); + + self.inner.catalog.insert( + key.clone(), + Item::new( + sequence, + Index::Inflight { + key: key.clone(), + value: value.clone(), + }, + ), + ); + + let flusher = sequence as usize % self.inner.flusher_entry_txs.len(); + self.inner.flusher_entry_txs[flusher] + .send(Entry { + sequence, + key, + value, + compression: writer.compression, + }) + .unwrap(); + + let duration = now.elapsed() + writer.duration; + self.inner + .metrics + .op_duration_insert_inserted + .observe(duration.as_secs_f64()); + + Ok(true) + } +} + +pub struct GenericStoreWriter +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + store: GenericStore, + /// `key` is always `Some` before `apply_writer`. + key: Option, + weight: usize, + + sequence: Option, + + judges: Judges, + is_judged: bool, + + /// judge duration + duration: Duration, + + is_inserted: bool, + is_skippable: bool, + compression: Compression, +} + +impl GenericStoreWriter +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + fn new(store: GenericStore, key: K, weight: usize) -> Self { + let judges = Judges::new(store.inner.admissions.len()); + let compression = store.inner.compression; + Self { + store, + key: Some(key), + weight, + sequence: None, + judges, + is_judged: false, + duration: Duration::from_nanos(0), + is_inserted: false, + is_skippable: false, + compression, + } + } + + /// Judge if the entry can be admitted by configured admission policies. + pub fn judge(&mut self) -> bool { + let store = self.store.clone(); + if !self.is_judged { + let now = Instant::now(); + store.judge_inner(self); + self.duration = now.elapsed(); + } + self.judges.judge() + } + + pub async fn finish(self, value: V) -> Result { + let store = self.store.clone(); + store.apply_writer(self, value).await + } + + pub fn force(&mut self) { + self.judges.set_mask(Bitmap::new()); + } + + pub fn set_judge_mask(&mut self, mask: Bitmap<64>) { + self.judges.set_mask(mask); + } + + pub fn set_skippable(&mut self) { + self.is_skippable = true + } + + pub fn set_sequence(&mut self, sequence: Sequence) { + self.sequence = Some(sequence); + } + + pub fn compression(&self) -> Compression { + self.compression + } + + pub fn set_compression(&mut self, compression: Compression) { + self.compression = compression + } +} + +impl Debug for GenericStoreWriter +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StoreWriter") + .field("key", &self.key) + .field("weight", &self.weight) + .field("judges", &self.judges) + .field("is_judged", &self.is_judged) + .field("duration", &self.duration) + .field("inserted", &self.is_inserted) + .finish() + } +} + +impl Drop for GenericStoreWriter +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + fn drop(&mut self) { + if !self.is_inserted { + debug_assert!(self.key.is_some()); + + let filtered = self.is_judged && !self.judge(); + // make sure each key after `judge` will call either `on_insert` or `on_drop`. + if self.is_judged { + for (i, admission) in self.store.inner.admissions.iter().enumerate() { + let judge = self.judges.get(i); + admission.on_drop(self.key.as_ref().unwrap(), self.weight, judge); + } + } + + if filtered { + self.store + .inner + .metrics + .op_duration_insert_filtered + .observe(self.duration.as_secs_f64()); + } else { + self.store + .inner + .metrics + .op_duration_insert_dropped + .observe(self.duration.as_secs_f64()); + } + } + } +} + +const ENTRY_MAGIC: u32 = 0x97_03_27_00; +const ENTRY_MAGIC_MASK: u32 = 0xFF_FF_FF_00; + +#[derive(Debug)] +pub struct EntryHeader { + pub key_len: u32, + pub value_len: u32, + pub sequence: Sequence, + pub checksum: u64, + pub compression: Compression, +} + +impl EntryHeader { + pub const fn serialized_len() -> usize { + 4 + 4 + 8 + 8 + 4 /* magic & compression */ + } + + pub fn write(&self, mut buf: &mut [u8]) { + buf.put_u32(self.key_len); + buf.put_u32(self.value_len); + buf.put_u64(self.sequence); + buf.put_u64(self.checksum); + + let v = ENTRY_MAGIC | self.compression.to_u8() as u32; + buf.put_u32(v); + } + + pub fn read(mut buf: &[u8]) -> Result { + let key_len = buf.get_u32(); + let value_len = buf.get_u32(); + let sequence = buf.get_u64(); + let checksum = buf.get_u64(); + + let v = buf.get_u32(); + let magic = v & ENTRY_MAGIC_MASK; + if magic != ENTRY_MAGIC { + return Err(anyhow!("magic mismatch, expected: {}, got: {}", ENTRY_MAGIC, magic).into()); + } + let compression = Compression::try_from(v as u8)?; + + Ok(Self { + key_len, + value_len, + sequence, + compression, + checksum, + }) + } +} + +/// | header | value (compressed) | key | | +/// +/// # Safety +/// +/// `buf.len()` must exactly fit entry size +fn read_entry(buf: &[u8]) -> Result<(K, V)> +where + K: Key, + V: Value, +{ + // read entry header + let header = EntryHeader::read(buf)?; + + // read value + let mut offset = EntryHeader::serialized_len(); + let compressed = &buf[offset..offset + header.value_len as usize]; + offset += header.value_len as usize; + let value = match header.compression { + Compression::None => V::read(compressed)?, + Compression::Zstd => { + let mut decompressed = Vec::with_capacity((header.value_len + header.value_len / 2) as usize); + zstd::stream::copy_decode(compressed, &mut decompressed).map_err(CodingError::from)?; + V::read(&decompressed[..])? + } + Compression::Lz4 => { + let mut decompressed = Vec::with_capacity((header.value_len + header.value_len / 2) as usize); + let mut decoder = lz4::Decoder::new(compressed).map_err(CodingError::from)?; + std::io::copy(&mut decoder, &mut decompressed).map_err(CodingError::from)?; + let (_r, res) = decoder.finish(); + res.map_err(CodingError::from)?; + V::read(&decompressed[..])? + } + }; + + // read key + let key = K::read(&buf[offset..offset + header.key_len as usize])?; + offset += header.key_len as usize; + + let checksum = checksum(&buf[EntryHeader::serialized_len()..offset]); + if checksum != header.checksum { + return Err(anyhow!("magic mismatch, expected: {}, got: {}", header.checksum, checksum).into()); + } + + Ok((key, value)) +} + +pub fn checksum(buf: &[u8]) -> u64 { + let mut hasher = XxHash64::with_seed(0); + hasher.write(buf); + hasher.finish() +} + +pub struct RegionEntryIter +where + K: Key, + V: Value, + D: Device, +{ + region: Region, + + cursor: usize, + + _marker: PhantomData<(K, V)>, +} + +impl RegionEntryIter +where + K: Key, + V: Value, + D: Device, +{ + pub async fn open(region: Region) -> Result> { + let align = region.device().align(); + + let slice = match region.load_range(..align).await? { + Some(slice) => slice, + None => return Ok(None), + }; + + let Ok(_) = RegionHeader::read(slice.as_ref()) else { + return Ok(None); + }; + + Ok(Some(Self { + region, + cursor: align, + _marker: PhantomData, + })) + } + + pub async fn next(&mut self) -> Result)>> { + let region_size = self.region.device().region_size(); + let align = self.region.device().align(); + + if self.cursor + align >= region_size { + return Ok(None); + } + + let Some(slice) = self.region.load_range(self.cursor..self.cursor + align).await? else { + return Ok(None); + }; + + let Ok(header) = EntryHeader::read(slice.as_ref()) else { + return Ok(None); + }; + + let entry_len = bits::align_up( + align, + (header.value_len + header.key_len) as usize + EntryHeader::serialized_len(), + ); + + let abs_start = self.cursor + EntryHeader::serialized_len() + header.value_len as usize; + let abs_end = self.cursor + EntryHeader::serialized_len() + (header.key_len + header.value_len) as usize; + + if abs_start >= abs_end || abs_end > region_size { + // Double check wrong entry. + return Ok(None); + } + + let align_start = bits::align_down(align, abs_start); + let align_end = bits::align_up(align, abs_end); + + let key = if align_start == self.cursor - align && align_end == self.cursor { + // header and key are in the same block, read directly from slice + let rel_start = EntryHeader::serialized_len() + header.value_len as usize; + let rel_end = rel_start + header.key_len as usize; + + let Ok(key) = K::read(&slice.as_ref()[rel_start..rel_end]) else { + return Ok(None); + }; + drop(slice); + key + } else { + drop(slice); + let Some(s) = self.region.load_range(align_start..align_end).await? else { + return Ok(None); + }; + let rel_start = abs_start - align_start; + let rel_end = abs_end - align_start; + + let Ok(key) = K::read(&s.as_ref()[rel_start..rel_end]) else { + return Ok(None); + }; + drop(s); + key + }; + + let info = Item::new( + header.sequence, + Index::Region { + view: self.region.view(self.cursor as u32, entry_len as u32), + }, + ); + + self.cursor += entry_len; + + Ok(Some((key, info))) + } + + pub async fn next_kv(&mut self) -> Result> { + let (_, item) = match self.next().await { + Ok(Some(res)) => res, + Ok(None) => return Ok(None), + Err(e) => return Err(e), + }; + + let Index::Region { view } = item.index() else { + unreachable!("kv loaded from region must have index of region") + }; + + // TODO(MrCroxx): Optimize if all key, value and footer are in the same read block. + let start = *view.offset() as usize; + let end = start + *view.len() as usize; + let Some(slice) = self.region.load_range(start..end).await? else { + return Ok(None); + }; + let kv = read_entry::(slice.as_ref()).ok(); + drop(slice); + + Ok(kv) + } +} + +impl StorageWriter for GenericStoreWriter +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + type Key = K; + type Value = V; + + fn key(&self) -> &Self::Key { + self.key.as_ref().unwrap() + } + + fn weight(&self) -> usize { + self.weight + } + + fn judge(&mut self) -> bool { + self.judge() + } + + fn force(&mut self) { + self.force() + } + + async fn finish(self, value: Self::Value) -> Result { + self.finish(value).await + } + + fn compression(&self) -> Compression { + self.compression() + } + + fn set_compression(&mut self, compression: Compression) { + self.set_compression(compression) + } +} + +impl Storage for GenericStore +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + type Key = K; + type Value = V; + type Config = GenericStoreConfig; + type Writer = GenericStoreWriter; + + async fn open(config: Self::Config) -> Result { + Self::open(config).await + } + + fn is_ready(&self) -> bool { + true + } + + async fn close(&self) -> Result<()> { + self.close().await + } + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer { + self.writer(key, weight) + } + + fn exists(&self, key: &Self::Key) -> Result { + self.exists(key) + } + + async fn lookup(&self, key: &Self::Key) -> Result> { + self.lookup(key).await + } + + fn remove(&self, key: &Self::Key) -> Result { + self.remove(key) + } + + fn clear(&self) -> Result<()> { + self.clear() + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use foyer_intrusive::eviction::fifo::{Fifo, FifoConfig, FifoLink}; + + use super::*; + use crate::{ + device::fs::{FsDevice, FsDeviceConfig}, + storage::StorageExt, + test_utils::JudgeRecorder, + }; + + type TestStore = GenericStore, FsDevice, Fifo>, FifoLink>; + + type TestStoreConfig = GenericStoreConfig, FsDevice, Fifo>>; + + #[tokio::test] + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::identity_op)] + async fn test_recovery() { + const KB: usize = 1024; + const MB: usize = 1024 * 1024; + + let tempdir = tempfile::tempdir().unwrap(); + + let recorder = Arc::new(JudgeRecorder::default()); + let admissions: Vec>>> = vec![recorder.clone()]; + let reinsertions: Vec>>> = vec![recorder.clone()]; + + let config = TestStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 16 * MB, + file_capacity: 4 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions, + reinsertions, + flushers: 1, + reclaimers: 1, + recover_concurrency: 2, + clean_region_threshold: 1, + compression: Compression::None, + }; + + let store = TestStore::open(config).await.unwrap(); + + // files: + // [0, 1, 2] + // [3, 4, 5] + // [6, 7, 8] + // [9, 10, 11] + // ... ... + for i in 0..21 { + store.insert(i, vec![i as u8; 1 * MB]).await.unwrap(); + } + + store.close().await.unwrap(); + + let remains = recorder.remains(); + + for i in 0..21 { + if remains.contains(&i) { + assert_eq!(store.lookup(&i).await.unwrap().unwrap(), vec![i as u8; 1 * MB],); + } else { + assert!(store.lookup(&i).await.unwrap().is_none()); + } + } + + drop(store); + + let config = TestStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 16 * MB, + file_capacity: 4 * MB, + align: 4096, + io_size: 4096 * KB, + }, + catalog_bits: 1, + admissions: vec![], + reinsertions: vec![], + flushers: 1, + reclaimers: 0, + recover_concurrency: 2, + clean_region_threshold: 1, + compression: Compression::None, + }; + let store = TestStore::open(config).await.unwrap(); + + for i in 0..21 { + if remains.contains(&i) { + assert_eq!(store.lookup(&i).await.unwrap().unwrap(), vec![i as u8; 1 * MB],); + } else { + assert!(store.lookup(&i).await.unwrap().is_none()); + } + } + + store.close().await.unwrap(); + + drop(store); + } +} diff --git a/foyer-storage/src/indices.rs b/foyer-storage/src/indices.rs new file mode 100644 index 00000000..ae20d276 --- /dev/null +++ b/foyer-storage/src/indices.rs @@ -0,0 +1,144 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeMap; + +use foyer_common::code::Key; +use itertools::Itertools; +use parking_lot::{RwLock, RwLockWriteGuard}; + +use crate::region::{RegionId, Version}; + +#[derive(Debug, Clone)] +pub struct Index +where + K: Key, +{ + pub key: K, + + pub region: RegionId, + pub version: Version, + pub offset: u32, + pub len: u32, + pub key_len: u32, + pub value_len: u32, +} + +#[derive(Debug, Clone, Copy)] +pub struct Slot { + pub region: RegionId, + pub sequence: u32, +} + +#[derive(Debug)] +struct IndicesInner +where + K: Key, +{ + slots: BTreeMap, + regions: Vec>>, + sequences: Vec, +} + +#[derive(Debug)] +pub struct Indices +where + K: Key, +{ + inner: RwLock>, +} + +impl Indices +where + K: Key, +{ + pub fn new(regions: usize) -> Self { + let inner = IndicesInner { + slots: BTreeMap::new(), + regions: vec![BTreeMap::new(); regions], + sequences: vec![0; regions], + }; + Self { + inner: RwLock::new(inner), + } + } + + pub fn insert(&self, index: Index) { + let mut inner = self.inner.write(); + self.insert_inner(&mut inner, index) + } + + pub fn lookup(&self, key: &K) -> Option> { + let inner = self.inner.read(); + let slot = inner.slots.get(key)?; + inner.regions[slot.region as usize] + .get(&slot.sequence) + .cloned() + } + + pub fn remap(&self, old_key: &K, new_key: K) -> bool { + let mut inner = self.inner.write(); + match self.remove_inner(&mut inner, old_key) { + Some(mut index) => { + index.key = new_key; + self.insert_inner(&mut inner, index); + true + } + None => false, + } + } + + pub fn remove(&self, key: &K) -> Option> { + let mut inner = self.inner.write(); + self.remove_inner(&mut inner, key) + } + + pub fn clear(&self) { + let mut inner = self.inner.write(); + inner.slots.clear(); + inner.regions.iter_mut().for_each(|region| region.clear()); + } + + pub fn take_region(&self, region: &RegionId) -> Vec> { + let mut inner = self.inner.write(); + let mut indices = BTreeMap::new(); + std::mem::swap(&mut indices, &mut inner.regions[*region as usize]); + + for index in indices.values() { + inner.slots.remove(&index.key); + } + + indices.into_values().collect_vec() + } + + fn insert_inner(&self, inner: &mut RwLockWriteGuard<'_, IndicesInner>, index: Index) { + let region = index.region; + let key = index.key.clone(); + + let sequence = inner.sequences[region as usize] as u32; + inner.sequences[region as usize] += 1; + + inner.regions[region as usize].insert(sequence, index); + inner.slots.insert(key, Slot { region, sequence }); + } + + fn remove_inner( + &self, + inner: &mut RwLockWriteGuard<'_, IndicesInner>, + key: &K, + ) -> Option> { + let slot = inner.slots.remove(key)?; + inner.regions[slot.region as usize].remove(&slot.sequence) + } +} diff --git a/foyer-storage/src/judge.rs b/foyer-storage/src/judge.rs new file mode 100644 index 00000000..b347cacd --- /dev/null +++ b/foyer-storage/src/judge.rs @@ -0,0 +1,95 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::ops::{BitAnd, BitOr}; + +use bitmaps::Bitmap; + +#[derive(Debug)] +pub struct Judges { + /// 1: admit + /// 0: reject + judge: Bitmap<64>, + /// 1: use + /// 0: ignore + umask: Bitmap<64>, +} + +impl Judges { + pub fn new(size: usize) -> Self { + let mut mask = Bitmap::default(); + mask.invert(); + Self::with_mask(size, mask) + } + + pub fn with_mask(size: usize, mask: Bitmap<64>) -> Self { + let mut umask = mask.bitand(Bitmap::from_value(1u64.wrapping_shl(size as u32).wrapping_sub(1))); + umask.invert(); + + Self { + judge: Bitmap::default(), + umask, + } + } + + pub fn get(&mut self, index: usize) -> bool { + self.judge.get(index) + } + + pub fn set(&mut self, index: usize, judge: bool) { + self.judge.set(index, judge); + } + + pub fn apply(&mut self, judge: Bitmap<64>) { + self.judge = judge; + } + + pub fn set_mask(&mut self, mut mask: Bitmap<64>) { + mask.invert(); + self.umask = mask; + } + + /// judge | ( ~mask ) + /// + /// | judge | mask | ~mask | result | + /// | 0 | 0 | 1 | 1 | + /// | 0 | 1 | 0 | 0 | + /// | 1 | 0 | 1 | 1 | + /// | 1 | 1 | 0 | 1 | + pub fn judge(&self) -> bool { + self.judge.bitor(self.umask).is_full() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_judge() { + let mask = Bitmap::from_value(0b_0011); + + let dataset = vec![ + (mask, Bitmap::from_value(0b_0011), true), + (mask, Bitmap::from_value(0b_1011), true), + (mask, Bitmap::from_value(0b_1010), false), + ]; + + for (i, (mask, j, e)) in dataset.into_iter().enumerate() { + let mut judge = Judges::with_mask(4, mask); + judge.apply(j); + assert_eq!(judge.judge(), e, "case {}, {} != {}", i, judge.judge(), e); + } + } +} diff --git a/foyer-storage/src/lazy.rs b/foyer-storage/src/lazy.rs new file mode 100644 index 00000000..a93f8542 --- /dev/null +++ b/foyer-storage/src/lazy.rs @@ -0,0 +1,307 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::{Arc, OnceLock}; + +use foyer_common::code::{Key, Value}; +use tokio::task::JoinHandle; + +use crate::{ + compress::Compression, + error::Result, + storage::{Storage, StorageWriter}, + store::{NoneStore, NoneStoreWriter, Store}, +}; + +#[derive(Debug)] +pub enum LazyStorageWriter +where + K: Key, + V: Value, + S: Storage, +{ + Store { writer: S::Writer }, + None { writer: NoneStoreWriter }, +} + +impl StorageWriter for LazyStorageWriter +where + K: Key, + V: Value, + S: Storage, +{ + type Key = K; + type Value = V; + + fn key(&self) -> &Self::Key { + match self { + LazyStorageWriter::Store { writer } => writer.key(), + LazyStorageWriter::None { writer } => writer.key(), + } + } + + fn weight(&self) -> usize { + match self { + LazyStorageWriter::Store { writer } => writer.weight(), + LazyStorageWriter::None { writer } => writer.weight(), + } + } + + fn judge(&mut self) -> bool { + match self { + LazyStorageWriter::Store { writer } => writer.judge(), + LazyStorageWriter::None { writer } => writer.judge(), + } + } + + fn force(&mut self) { + match self { + LazyStorageWriter::Store { writer } => writer.force(), + LazyStorageWriter::None { writer } => writer.force(), + } + } + + async fn finish(self, value: Self::Value) -> Result { + match self { + LazyStorageWriter::Store { writer } => writer.finish(value).await, + LazyStorageWriter::None { writer } => writer.finish(value).await, + } + } + + fn compression(&self) -> Compression { + match self { + LazyStorageWriter::Store { writer } => writer.compression(), + LazyStorageWriter::None { writer } => writer.compression(), + } + } + + fn set_compression(&mut self, compression: Compression) { + match self { + LazyStorageWriter::Store { writer } => writer.set_compression(compression), + LazyStorageWriter::None { writer } => writer.set_compression(compression), + } + } +} + +#[derive(Debug)] +pub struct LazyStorage +where + K: Key, + V: Value, + S: Storage, +{ + once: Arc>, + none: NoneStore, +} + +impl Clone for LazyStorage +where + K: Key, + V: Value, + S: Storage, +{ + fn clone(&self) -> Self { + Self { + once: Arc::clone(&self.once), + none: NoneStore::default(), + } + } +} + +impl LazyStorage +where + K: Key, + V: Value, + S: Storage, +{ + fn with_handle(config: S::Config) -> (Self, JoinHandle>) { + let once = Arc::new(OnceLock::new()); + + let handle = tokio::spawn({ + let once = once.clone(); + async move { + let store = match S::open(config).await { + Ok(store) => store, + Err(e) => { + tracing::error!("Lazy open store fail: {}", e); + return Err(e); + } + }; + once.set(store.clone()).unwrap(); + Ok(store) + } + }); + + let res = Self { + once, + none: NoneStore::default(), + }; + + (res, handle) + } +} + +impl Storage for LazyStorage +where + K: Key, + V: Value, + S: Storage, +{ + type Key = K; + type Value = V; + type Config = S::Config; + type Writer = LazyStorageWriter; + + async fn open(config: S::Config) -> Result { + let (store, task) = Self::with_handle(config); + tokio::spawn(task); + Ok(store) + } + + fn is_ready(&self) -> bool { + self.once.get().is_some() + } + + async fn close(&self) -> Result<()> { + match self.once.get() { + Some(store) => store.close().await, + None => self.none.close().await, + } + } + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer { + match self.once.get() { + Some(store) => LazyStorageWriter::Store { + writer: store.writer(key, weight), + }, + None => LazyStorageWriter::None { + writer: NoneStoreWriter::new(key, weight), + }, + } + } + + fn exists(&self, key: &Self::Key) -> Result { + match self.once.get() { + Some(store) => store.exists(key), + None => self.none.exists(key), + } + } + + async fn lookup(&self, key: &Self::Key) -> Result> { + match self.once.get() { + Some(store) => store.lookup(key).await, + None => self.none.lookup(key).await, + } + } + + fn remove(&self, key: &Self::Key) -> Result { + match self.once.get() { + Some(store) => store.remove(key), + None => self.none.remove(key), + } + } + + fn clear(&self) -> Result<()> { + match self.once.get() { + Some(store) => store.clear(), + None => self.none.clear(), + } + } +} + +pub type LazyStore = LazyStorage>; +pub type LazyStoreWriter = LazyStorageWriter>; + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use foyer_intrusive::eviction::fifo::FifoConfig; + + use super::*; + use crate::{ + device::fs::FsDeviceConfig, + storage::StorageExt, + store::{FifoFsStoreConfig, Store}, + }; + + const KB: usize = 1024; + const MB: usize = 1024 * 1024; + + #[tokio::test] + async fn test_lazy_store() { + let tempdir = tempfile::tempdir().unwrap(); + + let config = FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 16 * MB, + file_capacity: 4 * MB, + align: 4096, + io_size: 4096 * KB, + }, + catalog_bits: 1, + admissions: vec![], + reinsertions: vec![], + flushers: 1, + reclaimers: 1, + recover_concurrency: 2, + clean_region_threshold: 1, + compression: crate::compress::Compression::None, + }; + + let (store, handle) = LazyStorage::<_, _, Store<_, _>>::with_handle(config.into()); + + assert!(!store.insert(100, 100).await.unwrap()); + + handle.await.unwrap().unwrap(); + + assert!(store.insert(100, 100).await.unwrap()); + assert_eq!(store.lookup(&100).await.unwrap(), Some(100)); + + store.close().await.unwrap(); + drop(store); + + let config = FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 16 * MB, + file_capacity: 4 * MB, + align: 4096, + io_size: 4096 * KB, + }, + catalog_bits: 1, + admissions: vec![], + reinsertions: vec![], + flushers: 1, + reclaimers: 1, + recover_concurrency: 2, + clean_region_threshold: 1, + compression: crate::compress::Compression::None, + }; + + let (store, handle) = LazyStorage::<_, _, Store<_, _>>::with_handle(config.into()); + + assert!(store.lookup(&100).await.unwrap().is_none()); + + handle.await.unwrap().unwrap(); + + assert_eq!(store.lookup(&100).await.unwrap(), Some(100)); + store.close().await.unwrap(); + } +} diff --git a/foyer-storage/src/lib.rs b/foyer-storage/src/lib.rs new file mode 100644 index 00000000..3c54019b --- /dev/null +++ b/foyer-storage/src/lib.rs @@ -0,0 +1,34 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod admission; +pub mod buffer; +pub mod catalog; +pub mod compress; +pub mod device; +pub mod error; +pub mod flusher; +pub mod generic; +pub mod judge; +pub mod lazy; +pub mod metrics; +pub mod reclaimer; +pub mod region; +pub mod region_manager; +pub mod reinsertion; +pub mod runtime; +pub mod storage; +pub mod store; + +pub mod test_utils; diff --git a/foyer-storage/src/metrics.rs b/foyer-storage/src/metrics.rs new file mode 100644 index 00000000..a8334435 --- /dev/null +++ b/foyer-storage/src/metrics.rs @@ -0,0 +1,261 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::OnceLock; + +use prometheus::{ + core::{AtomicU64, GenericGauge, GenericGaugeVec}, + exponential_buckets, opts, register_histogram_vec_with_registry, register_int_counter_vec_with_registry, + register_int_gauge_vec_with_registry, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGaugeVec, Registry, +}; +type UintGaugeVec = GenericGaugeVec; +type UintGauge = GenericGauge; + +macro_rules! register_gauge_vec { + ($TYPE:ident, $OPTS:expr, $LABELS_NAMES:expr, $REGISTRY:expr $(,)?) => {{ + let gauge_vec = $TYPE::new($OPTS, $LABELS_NAMES).unwrap(); + $REGISTRY.register(Box::new(gauge_vec.clone())).map(|_| gauge_vec) + }}; +} + +macro_rules! register_uint_gauge_vec_with_registry { + ($OPTS:expr, $LABELS_NAMES:expr, $REGISTRY:expr $(,)?) => {{ + register_gauge_vec!(UintGaugeVec, $OPTS, $LABELS_NAMES, $REGISTRY) + }}; + + ($NAME:expr, $HELP:expr, $LABELS_NAMES:expr, $REGISTRY:expr $(,)?) => {{ + register_uint_gauge_vec_with_registry!(opts!($NAME, $HELP), $LABELS_NAMES, $REGISTRY) + }}; +} + +static REGISTRY: OnceLock = OnceLock::new(); + +/// Set metrics registry for `foyer`. +/// +/// Metrics registry must be set before `open`. +/// +/// Return `true` if set succeeds. +pub fn set_metrics_registry(registry: Registry) -> bool { + REGISTRY.set(registry).is_ok() +} + +pub fn get_metrics_registry() -> &'static Registry { + REGISTRY.get_or_init(|| prometheus::default_registry().clone()) +} + +// TODO(MrCroxx): Use `LazyLock` after `lazy_cell` is stable. +// /// Multiple foyer instance will share the same global metrics with different label `foyer` name. +// pub static METRICS: LazyLock = LazyLock::new(GlobalMetrics::default); + +lazy_static::lazy_static! { + pub static ref METRICS: GlobalMetrics = GlobalMetrics::default(); +} + +#[derive(Debug)] +pub struct GlobalMetrics { + op_duration: HistogramVec, + slow_op_duration: HistogramVec, + op_bytes: IntCounterVec, + total_bytes: UintGaugeVec, + + entry_bytes: HistogramVec, + + inner_op_duration: HistogramVec, + _inner_bytes: IntGaugeVec, +} + +impl Default for GlobalMetrics { + fn default() -> Self { + Self::new(get_metrics_registry()) + } +} + +impl GlobalMetrics { + pub fn new(registry: &Registry) -> Self { + let op_duration = register_histogram_vec_with_registry!( + "foyer_storage_op_duration", + "foyer storage op duration", + &["foyer", "op", "extra"], + vec![0.0001, 0.001, 0.005, 0.01, 0.02, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0], + registry, + ) + .unwrap(); + + let slow_op_duration = register_histogram_vec_with_registry!( + "foyer_storage_slow_op_duration", + "foyer storage slow op duration", + &["foyer", "op", "extra"], + vec![0.01, 0.1, 0.5, 0.77, 1.0, 2.5, 5.0, 7.5, 10.0], + registry, + ) + .unwrap(); + + let op_bytes = register_int_counter_vec_with_registry!( + "foyer_storage_op_bytes", + "foyer storage op bytes", + &["foyer", "op", "extra"], + registry, + ) + .unwrap(); + + let total_bytes = register_uint_gauge_vec_with_registry!( + "foyer_storage_total_bytes", + "foyer storage total bytes", + &["foyer"], + registry, + ) + .unwrap(); + + let entry_bytes = register_histogram_vec_with_registry!( + "foyer_storage_entry_bytes", + "foyer storage entry bytes", + &["foyer", "op", "extra"], + exponential_buckets(1.0, 2.0, 32).unwrap(), + registry, + ) + .unwrap(); + + let inner_op_duration = register_histogram_vec_with_registry!( + "foyer_storage_inner_op_duration", + "foyer storage inner op duration", + &["foyer", "op", "extra"], + vec![0.0001, 0.01, 0.02, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 10.0], + registry, + ) + .unwrap(); + + let inner_bytes = register_int_gauge_vec_with_registry!( + "foyer_storage_inner_bytes", + "foyer storage inner bytes", + &["foyer", "component", "extra"], + registry, + ) + .unwrap(); + + Self { + op_duration, + slow_op_duration, + op_bytes, + total_bytes, + + entry_bytes, + + inner_op_duration, + _inner_bytes: inner_bytes, + } + } + + pub fn foyer(&self, name: &str) -> Metrics { + Metrics::new(self, name) + } +} + +#[derive(Debug)] +pub struct Metrics { + pub op_duration_insert_inserted: Histogram, + pub op_duration_insert_filtered: Histogram, + pub op_duration_insert_dropped: Histogram, + pub op_duration_lookup_hit: Histogram, + pub op_duration_lookup_miss: Histogram, + pub op_duration_remove: Histogram, + pub slow_op_duration_reclaim: Histogram, + + pub op_bytes_insert: IntCounter, + pub op_bytes_lookup: IntCounter, + pub op_bytes_flush: IntCounter, + pub op_bytes_reclaim: IntCounter, + pub op_bytes_reinsert: IntCounter, + + pub total_bytes: UintGauge, + + pub insert_entry_bytes: Histogram, + + pub inner_op_duration_acquire_clean_region: Histogram, + pub inner_op_duration_acquire_clean_buffer: Histogram, + pub inner_op_duration_wait_ring_buffer: Histogram, + pub inner_op_duration_update_catalog: Histogram, + pub inner_op_duration_entry_flush: Histogram, + pub inner_op_duration_flusher_handle: Histogram, +} + +impl Metrics { + pub fn new(global: &GlobalMetrics, foyer: &str) -> Self { + let op_duration_insert_inserted = global.op_duration.with_label_values(&[foyer, "insert", "inserted"]); + let op_duration_insert_filtered = global.op_duration.with_label_values(&[foyer, "insert", "filtered"]); + let op_duration_insert_dropped = global.op_duration.with_label_values(&[foyer, "insert", "dropped"]); + let op_duration_lookup_hit = global.op_duration.with_label_values(&[foyer, "lookup", "hit"]); + let op_duration_lookup_miss = global.op_duration.with_label_values(&[foyer, "lookup", "miss"]); + let op_duration_remove = global.op_duration.with_label_values(&[foyer, "remove", ""]); + let slow_op_duration_reclaim = global.slow_op_duration.with_label_values(&[foyer, "reclaim", ""]); + + let op_bytes_insert = global.op_bytes.with_label_values(&[foyer, "insert", ""]); + let op_bytes_lookup = global.op_bytes.with_label_values(&[foyer, "lookup", ""]); + let op_bytes_flush = global.op_bytes.with_label_values(&[foyer, "flush", ""]); + let op_bytes_reclaim = global.op_bytes.with_label_values(&[foyer, "reclaim", ""]); + let op_bytes_reinsert = global.op_bytes.with_label_values(&[foyer, "reinsert", ""]); + + let total_bytes = global.total_bytes.with_label_values(&[foyer]); + + let insert_entry_bytes = global.entry_bytes.with_label_values(&[foyer, "insert", ""]); + + let inner_op_duration_acquire_clean_region = + global + .inner_op_duration + .with_label_values(&[foyer, "acquire_clean_region", ""]); + let inner_op_duration_acquire_clean_buffer = + global + .inner_op_duration + .with_label_values(&[foyer, "acquire_clean_buffer", ""]); + let inner_op_duration_wait_ring_buffer = + global + .inner_op_duration + .with_label_values(&[foyer, "wait_ring_buffer", ""]); + let inner_op_duration_update_catalog = + global + .inner_op_duration + .with_label_values(&[foyer, "update_catalog", ""]); + let inner_op_duration_entry_flush = global.inner_op_duration.with_label_values(&[foyer, "entry_flush", ""]); + let inner_op_duration_flusher_handle = + global + .inner_op_duration + .with_label_values(&[foyer, "flusher_handle", ""]); + + Self { + op_duration_insert_inserted, + op_duration_insert_filtered, + op_duration_insert_dropped, + op_duration_lookup_hit, + op_duration_lookup_miss, + op_duration_remove, + slow_op_duration_reclaim, + + op_bytes_insert, + op_bytes_lookup, + op_bytes_flush, + op_bytes_reclaim, + op_bytes_reinsert, + + total_bytes, + + insert_entry_bytes, + + inner_op_duration_acquire_clean_region, + inner_op_duration_acquire_clean_buffer, + inner_op_duration_wait_ring_buffer, + inner_op_duration_update_catalog, + inner_op_duration_entry_flush, + inner_op_duration_flusher_handle, + } + } +} diff --git a/foyer-storage/src/reclaimer.rs b/foyer-storage/src/reclaimer.rs new file mode 100644 index 00000000..864cb66a --- /dev/null +++ b/foyer-storage/src/reclaimer.rs @@ -0,0 +1,220 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + sync::{atomic::Ordering, Arc}, + time::Duration, +}; + +use bytes::BufMut; +use foyer_common::code::{Key, Value}; +use foyer_intrusive::{core::adapter::Link, eviction::EvictionPolicy}; +use tokio::sync::broadcast; + +use crate::{ + device::Device, + error::Result, + generic::{GenericStore, RegionEntryIter}, + judge::Judges, + metrics::Metrics, + region_manager::{RegionEpItemAdapter, RegionManager}, + storage::Storage, +}; + +#[derive(Debug)] +pub struct Reclaimer +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + threshold: usize, + + store: GenericStore, + + region_manager: Arc>, + + metrics: Arc, + + stop_rx: broadcast::Receiver<()>, +} + +impl Reclaimer +where + K: Key, + V: Value, + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + pub fn new( + threshold: usize, + store: GenericStore, + region_manager: Arc>, + metrics: Arc, + stop_rx: broadcast::Receiver<()>, + ) -> Self { + Self { + threshold, + store, + region_manager, + metrics, + stop_rx, + } + } + + pub async fn run(mut self) -> Result<()> { + let mut watch = self.region_manager.clean_regions().watch(); + loop { + tokio::select! { + biased; + Ok(()) = watch.changed() => { + self.handle().await?; + } + _ = self.stop_rx.recv() => { + tracing::info!("[reclaimer] exit"); + return Ok(()) + } + } + } + } + + async fn handle(&self) -> Result<()> { + if self.region_manager.clean_regions().len() >= self.threshold { + return Ok(()); + } + + // TODO(MrCroxx): subscribe evictable region changes. + let region_id = loop { + match self.region_manager.eviction_pop() { + Some(id) => break id, + None => tokio::time::sleep(Duration::from_millis(100)).await, + } + }; + + let _timer = self.metrics.slow_op_duration_reclaim.start_timer(); + + let region = self.region_manager.region(®ion_id); + + // step 1: drop indices + let indices = self.store.catalog().take_region(®ion_id); + + // Must guarantee there is no following reads on the region to be reclaim. + // Which means there is no unfinished reader or reader who holds index and prepare to read. + + // wait unfinished readers + { + // only each `indices` holds one ref + while region.refs().load(Ordering::SeqCst) > indices.len() { + tokio::time::sleep(Duration::from_millis(1)).await; + } + } + + // step 2: do reinsertion + let reinsert = || { + let region = region.clone(); + let metrics = self.metrics.clone(); + let reinsertions = self.store.reinsertions().clone(); + + tracing::info!("[reclaimer] begin reinsertion, region: {}", region_id); + + async move { + let mut iter = match RegionEntryIter::::open(region).await { + Ok(Some(iter)) => iter, + Ok(None) => return Ok(true), + Err(e) => return Err(e), + }; + + while let Some((key, value)) = iter.next_kv().await? { + let weight = key.serialized_len() + value.serialized_len(); + + let mut judges = Judges::new(reinsertions.len()); + for (index, reinsertion) in reinsertions.iter().enumerate() { + let judge = reinsertion.judge(&key, weight); + judges.set(index, judge); + } + if !judges.judge() { + for (index, reinsertion) in reinsertions.iter().enumerate() { + let judge = judges.get(index); + reinsertion.on_drop(&key, weight, judge); + } + continue; + } + + let mut writer = self.store.writer(key.clone(), weight); + writer.set_skippable(); + + if !writer.judge() { + continue; + } + + if writer.finish(value).await? { + for (index, reinsertion) in reinsertions.iter().enumerate() { + let judge = judges.get(index); + reinsertion.on_insert(&key, weight, judge); + } + } else { + for (index, reinsertion) in reinsertions.iter().enumerate() { + let judge = judges.get(index); + reinsertion.on_drop(&key, weight, judge); + } + // The writer is already been judged and admitted, but not inserted successfully and skipped. + // That means allocating timeouts and there is no clean region available. + // Reinsertion should be interrupted to make sure foreground insertion. + return Ok(false); + } + + metrics.op_bytes_reinsert.inc_by(weight as u64); + } + + tracing::info!("[reclaimer] finish reinsertion, region: {}", region_id); + + Ok(true) + } + }; + + if !self.store.reinsertions().is_empty() { + match reinsert().await { + Ok(true) => { + tracing::info!("[reclaimer] reinsertion finish, region: {}", region_id) + } + Ok(false) => { + tracing::info!("[reclaimer] reinsertion skipped, region: {}", region_id) + } + Err(e) => tracing::warn!("reinsert region {:?} error: {:?}", region, e), + } + } + + // step 3: wipe region header + let align = region.device().align(); + let mut buf = region.device().io_buffer(align, align); + (&mut buf[..]).put_slice(&vec![0; align]); + let (res, _buf) = region.device().write(buf, .., region_id, 0).await; + res?; + + // step 4: send clean region + self.region_manager.clean_regions().release(region_id); + + tracing::info!("[reclaimer] finish reclaim task, region: {}", region_id); + + self.metrics + .op_bytes_reclaim + .inc_by(region.device().region_size() as u64); + self.metrics.total_bytes.sub(region.device().region_size() as u64); + + Ok(()) + } +} diff --git a/foyer-storage/src/region.rs b/foyer-storage/src/region.rs new file mode 100644 index 00000000..ef98e1d2 --- /dev/null +++ b/foyer-storage/src/region.rs @@ -0,0 +1,292 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + collections::btree_map::{BTreeMap, Entry}, + fmt::Debug, + ops::RangeBounds, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +use allocator_api2::vec::Vec as VecA; +use bytes::{Buf, BufMut}; +use foyer_common::range::RangeBoundsExt; +use parking_lot::Mutex; +use tokio::sync::oneshot; + +use crate::{ + device::{BufferAllocator, Device, DeviceExt}, + error::Result, +}; + +pub type RegionId = u32; + +pub const REGION_MAGIC: u64 = 0x19970327; + +#[derive(Debug)] +pub enum Version { + V1, +} + +impl Version { + pub fn latest() -> Self { + Self::V1 + } + + pub fn to_u64(&self) -> u64 { + match self { + Version::V1 => 1, + } + } +} + +impl From for u64 { + fn from(value: Version) -> Self { + match value { + Version::V1 => 1, + } + } +} + +impl TryFrom for Version { + type Error = anyhow::Error; + + fn try_from(value: u64) -> std::result::Result { + match value { + 1 => Ok(Self::V1), + v => Err(anyhow::anyhow!("invalid region format version: {}", v)), + } + } +} + +#[derive(Debug)] +pub struct RegionHeader { + /// magic number to decide a valid region + pub magic: u64, + /// format version + pub version: Version, +} + +impl RegionHeader { + pub fn write(&self, mut buf: &mut [u8]) { + buf.put_u64(self.magic); + buf.put_u64(self.version.to_u64()); + } + + pub fn read(mut buf: &[u8]) -> std::result::Result { + let magic = buf.get_u64(); + if magic != REGION_MAGIC { + return Err(anyhow::anyhow!( + "region magic mismatch, magic: {}, expected: {}", + magic, + REGION_MAGIC + )); + } + let version = buf.get_u64().try_into()?; + Ok(Self { magic, version }) + } +} + +#[derive(Debug)] +pub struct RegionInner +where + A: BufferAllocator, +{ + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::type_complexity)] + waits: BTreeMap<(usize, usize), Vec>>>>>, +} + +#[derive(Debug, Clone)] +pub struct Region +where + D: Device, +{ + id: RegionId, + + inner: Arc>>, + + device: D, + + refs: Arc, +} + +impl Region +where + D: Device, +{ + pub fn new(id: RegionId, device: D) -> Self { + let inner = RegionInner { waits: BTreeMap::new() }; + Self { + id, + inner: Arc::new(Mutex::new(inner)), + device, + refs: Arc::new(AtomicUsize::default()), + } + } + + pub fn view(&self, offset: u32, len: u32) -> RegionView { + self.refs.fetch_add(1, Ordering::SeqCst); + RegionView { + id: self.id, + offset, + len, + refs: Arc::clone(&self.refs), + } + } + + pub fn refs(&self) -> &Arc { + &self.refs + } + + /// Load region data by view from device. + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::type_complexity)] + #[tracing::instrument(skip(self, view))] + pub async fn load(&self, view: RegionView) -> Result>>> { + let res = self + .load_range(view.offset as usize..view.offset as usize + view.len as usize) + .await; + // drop view after load finish + drop(view); + res + } + + /// Load region data with given `range` from device. + // TODO(MrCroxx): use `expect` after `lint_reasons` is stable. + #[allow(clippy::type_complexity)] + #[tracing::instrument(skip(self, range), fields(start, end))] + pub async fn load_range( + &self, + range: impl RangeBounds, + ) -> Result>>> { + let range = range.bounds(0..self.device.region_size()); + + let rx = { + let mut inner = self.inner.lock(); + + // join wait map if exists + let rx = match inner.waits.entry((range.start, range.end)) { + Entry::Vacant(v) => { + v.insert(vec![]); + None + } + Entry::Occupied(mut o) => { + let (tx, rx) = oneshot::channel(); + o.get_mut().push(tx); + Some(rx) + } + }; + + drop(inner); + + rx + }; + + // wait for result if joined into wait map + if let Some(rx) = rx { + return rx.await.map_err(anyhow::Error::from)?.map(Some); + } + + // otherwise, read from device + let region = self.id; + + let buf = match self.device.load(region, range.start..range.end).await { + Err(e) => { + self.cleanup(range.start, range.end)?; + return Err(e.into()); + } + Ok(buf) if buf.len() != range.size().unwrap() => { + self.cleanup(range.start, range.end)?; + return Ok(None); + } + Ok(buf) => buf, + }; + let buf = Arc::new(buf); + + if let Some(txs) = self.inner.lock().waits.remove(&(range.start, range.end)) { + for tx in txs { + tx.send(Ok(buf.clone())).unwrap() + } + } + + Ok(Some(buf)) + } + + pub fn id(&self) -> RegionId { + self.id + } + + pub fn device(&self) -> &D { + &self.device + } + + /// Cleanup waits. + fn cleanup(&self, start: usize, end: usize) -> Result<()> { + if let Some(txs) = self.inner.lock().waits.remove(&(start, end)) { + for tx in txs { + tx.send(Err(anyhow::anyhow!("cancelled by previous error").into())) + .unwrap() + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct RegionView { + id: RegionId, + offset: u32, + len: u32, + refs: Arc, +} + +impl Clone for RegionView { + fn clone(&self) -> Self { + self.refs.fetch_add(1, Ordering::SeqCst); + Self { + id: self.id, + offset: self.offset, + len: self.len, + refs: Arc::clone(&self.refs), + } + } +} + +impl Drop for RegionView { + fn drop(&mut self) { + self.refs.fetch_sub(1, Ordering::SeqCst); + } +} + +impl RegionView { + pub fn id(&self) -> &RegionId { + &self.id + } + + pub fn offset(&self) -> &u32 { + &self.offset + } + + pub fn len(&self) -> &u32 { + &self.len + } + + pub fn refs(&self) -> &Arc { + &self.refs + } +} diff --git a/foyer-storage/src/region_manager.rs b/foyer-storage/src/region_manager.rs new file mode 100644 index 00000000..393b1493 --- /dev/null +++ b/foyer-storage/src/region_manager.rs @@ -0,0 +1,115 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use foyer_common::async_queue::AsyncQueue; +use foyer_intrusive::{ + core::adapter::Link, + eviction::{EvictionPolicy, EvictionPolicyExt}, + intrusive_adapter, key_adapter, +}; +use parking_lot::RwLock; + +use crate::{ + device::Device, + region::{Region, RegionId}, +}; + +#[derive(Debug)] +pub struct RegionEpItem +where + L: Link, +{ + link: L, + id: RegionId, +} + +intrusive_adapter! { pub RegionEpItemAdapter = Arc>: RegionEpItem { link: L } where L: Link } +key_adapter! { RegionEpItemAdapter = RegionEpItem { id: RegionId } where L: Link } + +#[derive(Debug)] +pub struct RegionManager +where + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + /// Empty regions. + clean_regions: AsyncQueue, + + regions: Vec>, + items: Vec>>, + + /// Eviction policy. + eviction: RwLock, +} + +impl RegionManager +where + D: Device, + EP: EvictionPolicy>, + EL: Link, +{ + pub fn new(region_count: usize, eviction_config: EP::Config, device: D) -> Self { + let eviction = EP::new(eviction_config); + let clean_regions = AsyncQueue::new(); + + let mut regions = Vec::with_capacity(region_count); + let mut items = Vec::with_capacity(region_count); + + for id in 0..region_count as RegionId { + let region = Region::new(id, device.clone()); + let item = Arc::new(RegionEpItem { + link: EL::default(), + id, + }); + + regions.push(region); + items.push(item); + } + + Self { + clean_regions, + regions, + items, + eviction: RwLock::new(eviction), + } + } + + pub fn region(&self, id: &RegionId) -> &Region { + &self.regions[*id as usize] + } + + #[tracing::instrument(skip(self))] + pub fn record_access(&self, id: &RegionId) { + let mut eviction = self.eviction.write(); + let item = &self.items[*id as usize]; + if item.link.is_linked() { + eviction.access(&self.items[*id as usize]); + } + } + + pub fn clean_regions(&self) -> &AsyncQueue { + &self.clean_regions + } + + pub fn eviction_push(&self, region_id: RegionId) { + self.eviction.write().push(self.items[region_id as usize].clone()); + } + + pub fn eviction_pop(&self) -> Option { + self.eviction.write().pop().map(|item| item.id) + } +} diff --git a/foyer-storage/src/reinsertion/exist.rs b/foyer-storage/src/reinsertion/exist.rs new file mode 100644 index 00000000..d1764ddb --- /dev/null +++ b/foyer-storage/src/reinsertion/exist.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::{Arc, OnceLock}; + +use foyer_common::code::{Key, Value}; + +use super::{ReinsertionContext, ReinsertionPolicy}; +use crate::catalog::Catalog; + +#[derive(Debug)] +pub struct ExistReinsertionPolicy +where + K: Key, + V: Value, +{ + catalog: OnceLock>>, +} + +impl Default for ExistReinsertionPolicy +where + K: Key, + V: Value, +{ + fn default() -> Self { + Self { + catalog: OnceLock::new(), + } + } +} + +impl ReinsertionPolicy for ExistReinsertionPolicy +where + K: Key, + V: Value, +{ + type Key = K; + + type Value = V; + + fn init(&self, context: ReinsertionContext) { + self.catalog.get_or_init(|| context.catalog.clone()); + } + + fn judge(&self, key: &Self::Key, _weight: usize) -> bool { + let indices = self.catalog.get().unwrap(); + indices.lookup(key).is_some() + } + + fn on_insert(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} + + fn on_drop(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} +} diff --git a/foyer-storage/src/reinsertion/mod.rs b/foyer-storage/src/reinsertion/mod.rs new file mode 100644 index 00000000..7681863e --- /dev/null +++ b/foyer-storage/src/reinsertion/mod.rs @@ -0,0 +1,58 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{fmt::Debug, sync::Arc}; + +use foyer_common::code::{Key, Value}; + +use crate::{catalog::Catalog, metrics::Metrics}; + +#[derive(Debug)] +pub struct ReinsertionContext +where + K: Key, + V: Value, +{ + pub catalog: Arc>, + pub metrics: Arc, +} + +impl Clone for ReinsertionContext +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + Self { + catalog: self.catalog.clone(), + metrics: self.metrics.clone(), + } + } +} + +pub trait ReinsertionPolicy: Send + Sync + 'static + Debug { + type Key: Key; + type Value: Value; + + fn init(&self, context: ReinsertionContext); + + fn judge(&self, key: &Self::Key, weight: usize) -> bool; + + fn on_insert(&self, key: &Self::Key, weight: usize, judge: bool); + + fn on_drop(&self, key: &Self::Key, weight: usize, judge: bool); +} + +pub mod exist; +pub mod rated_ticket; diff --git a/foyer-storage/src/reinsertion/rated_ticket.rs b/foyer-storage/src/reinsertion/rated_ticket.rs new file mode 100644 index 00000000..ccc39027 --- /dev/null +++ b/foyer-storage/src/reinsertion/rated_ticket.rs @@ -0,0 +1,88 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + fmt::Debug, + sync::{ + atomic::{AtomicUsize, Ordering}, + OnceLock, + }, +}; + +use foyer_common::{ + code::{Key, Value}, + rated_ticket::RatedTicket, +}; + +use super::{ReinsertionContext, ReinsertionPolicy}; + +#[derive(Debug)] +pub struct RatedTicketReinsertionPolicy +where + K: Key, + V: Value, +{ + inner: RatedTicket, + + last: AtomicUsize, + + context: OnceLock>, +} + +impl RatedTicketReinsertionPolicy +where + K: Key, + V: Value, +{ + pub fn new(rate: usize) -> Self { + Self { + inner: RatedTicket::new(rate as f64), + last: AtomicUsize::default(), + context: OnceLock::new(), + } + } +} + +impl ReinsertionPolicy for RatedTicketReinsertionPolicy +where + K: Key, + V: Value, +{ + type Key = K; + type Value = V; + + fn init(&self, context: super::ReinsertionContext) { + self.context.set(context).unwrap(); + } + + fn judge(&self, _key: &Self::Key, _weight: usize) -> bool { + let res = self.inner.probe(); + + let metrics = self.context.get().unwrap().metrics.as_ref(); + let current = metrics.op_bytes_reinsert.get() as usize; + let last = self.last.load(Ordering::Relaxed); + let delta = current.saturating_sub(last); + + if delta > 0 { + self.last.store(current, Ordering::Relaxed); + self.inner.reduce(delta as f64); + } + + res + } + + fn on_insert(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} + + fn on_drop(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} +} diff --git a/foyer-storage/src/runtime.rs b/foyer-storage/src/runtime.rs new file mode 100644 index 00000000..6212172e --- /dev/null +++ b/foyer-storage/src/runtime.rs @@ -0,0 +1,212 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use foyer_common::{ + code::{Key, Value}, + runtime::BackgroundShutdownRuntime, +}; + +use crate::{ + compress::Compression, + error::Result, + lazy::LazyStore, + storage::{Storage, StorageWriter}, + store::Store, +}; + +#[derive(Debug, Clone)] +pub struct RuntimeConfig { + pub worker_threads: Option, + pub thread_name: Option, +} + +#[derive(Debug)] +pub struct RuntimeStorageConfig +where + K: Key, + V: Value, + S: Storage, +{ + pub store: S::Config, + pub runtime: RuntimeConfig, +} + +impl Clone for RuntimeStorageConfig +where + K: Key, + V: Value, + S: Storage, +{ + fn clone(&self) -> Self { + Self { + store: self.store.clone(), + runtime: self.runtime.clone(), + } + } +} + +#[derive(Debug)] +pub struct RuntimeStorageWriter +where + K: Key, + V: Value, + S: Storage, +{ + runtime: Arc, + writer: S::Writer, +} + +impl StorageWriter for RuntimeStorageWriter +where + K: Key, + V: Value, + S: Storage, +{ + type Key = K; + type Value = V; + + fn key(&self) -> &Self::Key { + self.writer.key() + } + + fn weight(&self) -> usize { + self.writer.weight() + } + + fn judge(&mut self) -> bool { + self.writer.judge() + } + + fn force(&mut self) { + self.writer.force() + } + + async fn finish(self, value: Self::Value) -> Result { + self.runtime + .spawn(async move { self.writer.finish(value).await }) + .await + .unwrap() + } + + fn compression(&self) -> Compression { + self.writer.compression() + } + + fn set_compression(&mut self, compression: Compression) { + self.writer.set_compression(compression) + } +} + +#[derive(Debug)] +pub struct RuntimeStorage +where + K: Key, + V: Value, + S: Storage, +{ + runtime: Arc, + store: S, +} + +impl Clone for RuntimeStorage +where + K: Key, + V: Value, + S: Storage, +{ + fn clone(&self) -> Self { + Self { + runtime: Arc::clone(&self.runtime), + store: self.store.clone(), + } + } +} + +impl Storage for RuntimeStorage +where + K: Key, + V: Value, + S: Storage, +{ + type Key = K; + type Value = V; + type Config = RuntimeStorageConfig; + type Writer = RuntimeStorageWriter; + + async fn open(config: Self::Config) -> Result { + let mut builder = tokio::runtime::Builder::new_multi_thread(); + if let Some(worker_threads) = config.runtime.worker_threads { + builder.worker_threads(worker_threads); + } + if let Some(thread_name) = config.runtime.thread_name { + builder.thread_name(thread_name); + } + let runtime = builder.enable_all().build().map_err(anyhow::Error::from)?; + let runtime = BackgroundShutdownRuntime::from(runtime); + let runtime = Arc::new(runtime); + let store = runtime + .spawn(async move { S::open(config.store).await }) + .await + .unwrap()?; + Ok(Self { runtime, store }) + } + + fn is_ready(&self) -> bool { + self.store.is_ready() + } + + async fn close(&self) -> Result<()> { + let store = self.store.clone(); + self.runtime.spawn(async move { store.close().await }).await.unwrap() + } + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer { + let writer = self.store.writer(key, weight); + RuntimeStorageWriter { + runtime: self.runtime.clone(), + writer, + } + } + + fn exists(&self, key: &Self::Key) -> crate::error::Result { + self.store.exists(key) + } + + async fn lookup(&self, key: &Self::Key) -> Result> { + let store = self.store.clone(); + let key = key.clone(); + self.runtime + .spawn(async move { store.lookup(&key).await }) + .await + .unwrap() + } + + fn remove(&self, key: &Self::Key) -> crate::error::Result { + self.store.remove(key) + } + + fn clear(&self) -> crate::error::Result<()> { + self.store.clear() + } +} + +pub type RuntimeStore = RuntimeStorage>; +pub type RuntimeStoreWriter = RuntimeStorageWriter>; +pub type RuntimeStoreConfig = RuntimeStorageConfig>; + +pub type RuntimeLazyStore = RuntimeStorage>; +pub type RuntimeLazyStoreWriter = RuntimeStorageWriter>; +pub type RuntimeLazyStoreConfig = RuntimeStorageConfig>; diff --git a/foyer-storage/src/storage.rs b/foyer-storage/src/storage.rs new file mode 100644 index 00000000..0e9a70d6 --- /dev/null +++ b/foyer-storage/src/storage.rs @@ -0,0 +1,473 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Debug; + +use foyer_common::code::{Key, Value}; +use futures::Future; + +use crate::{compress::Compression, error::Result}; + +// TODO(MrCroxx): Use `trait_alias` after stable. +// pub trait FetchValueFuture = Future> + Send + 'static; + +pub trait FetchValueFuture: Future> + Send + 'static {} +impl> + Send + 'static> FetchValueFuture for T {} + +pub trait StorageWriter: Send + Sync + Debug { + type Key: Key; + type Value: Value; + + fn key(&self) -> &Self::Key; + + fn weight(&self) -> usize; + + fn judge(&mut self) -> bool; + + fn force(&mut self); + + fn compression(&self) -> Compression; + + fn set_compression(&mut self, compression: Compression); + + fn finish(self, value: Self::Value) -> impl Future> + Send; +} + +pub trait Storage: Send + Sync + Debug + Clone + 'static { + type Key: Key; + type Value: Value; + type Config: Send + Clone + Debug; + type Writer: StorageWriter; + + #[must_use] + fn open(config: Self::Config) -> impl Future> + Send; + + fn is_ready(&self) -> bool; + + #[must_use] + fn close(&self) -> impl Future> + Send; + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer; + + fn exists(&self, key: &Self::Key) -> Result; + + #[must_use] + fn lookup(&self, key: &Self::Key) -> impl Future>> + Send; + + fn remove(&self, key: &Self::Key) -> Result; + + fn clear(&self) -> Result<()>; +} + +pub trait StorageExt: Storage { + #[must_use] + #[tracing::instrument(skip(self, value))] + fn insert(&self, key: Self::Key, value: Self::Value) -> impl Future> + Send { + let weight = key.serialized_len() + value.serialized_len(); + self.writer(key, weight).finish(value) + } + + #[must_use] + #[tracing::instrument(skip(self, value))] + fn insert_if_not_exists(&self, key: Self::Key, value: Self::Value) -> impl Future> + Send { + async move { + if self.exists(&key)? { + return Ok(false); + } + self.insert(key, value).await + } + } + + /// First judge if the entry will be admitted with `key` and `weight` by admission policies. + /// Then `f` will be called and entry will be inserted. + /// + /// # Safety + /// + /// `weight` MUST be equal to `key.serialized_len() + value.serialized_len()` + #[must_use] + #[tracing::instrument(skip(self, f))] + fn insert_with(&self, key: Self::Key, f: F, weight: usize) -> impl Future> + Send + where + F: FnOnce() -> anyhow::Result + Send, + { + async move { + let mut writer = self.writer(key, weight); + if !writer.judge() { + return Ok(false); + } + let value = match f() { + Ok(value) => value, + Err(e) => { + tracing::warn!("fetch value error: {:?}", e); + return Ok(false); + } + }; + writer.finish(value).await + } + } + + /// First judge if the entry will be admitted with `key` and `weight` by admission policies. + /// Then `f` will be called to fetch value, and entry will be inserted. + /// + /// # Safety + /// + /// `weight` MUST be equal to `key.serialized_len() + value.serialized_len()` + #[tracing::instrument(skip(self, f))] + fn insert_with_future( + &self, + key: Self::Key, + f: F, + weight: usize, + ) -> impl Future> + Send + where + F: FnOnce() -> FU + Send, + FU: FetchValueFuture, + { + async move { + let mut writer = self.writer(key, weight); + if !writer.judge() { + return Ok(false); + } + let value = match f().await { + Ok(value) => value, + Err(e) => { + tracing::warn!("fetch value error: {:?}", e); + return Ok(false); + } + }; + writer.finish(value).await + } + } + + #[tracing::instrument(skip(self, f))] + fn insert_if_not_exists_with( + &self, + key: Self::Key, + f: F, + weight: usize, + ) -> impl Future> + Send + where + F: FnOnce() -> anyhow::Result + Send, + { + async move { + if self.exists(&key)? { + return Ok(false); + } + self.insert_with(key, f, weight).await + } + } + + #[tracing::instrument(skip(self, f))] + fn insert_if_not_exists_with_future( + &self, + key: Self::Key, + f: F, + weight: usize, + ) -> impl Future> + Send + where + F: FnOnce() -> FU + Send, + FU: FetchValueFuture, + { + async move { + if self.exists(&key)? { + return Ok(false); + } + self.insert_with_future(key, f, weight).await + } + } +} + +impl StorageExt for S {} + +pub trait AsyncStorageExt: Storage { + #[tracing::instrument(skip(self, value))] + fn insert_async(&self, key: Self::Key, value: Self::Value) { + let store = self.clone(); + tokio::spawn(async move { + if let Err(e) = store.insert(key, value).await { + tracing::warn!("async storage insert error: {}", e); + } + }); + } + + #[tracing::instrument(skip(self, value))] + fn insert_if_not_exists_async(&self, key: Self::Key, value: Self::Value) { + let store = self.clone(); + tokio::spawn(async move { + if let Err(e) = store.insert_if_not_exists(key, value).await { + tracing::warn!("async storage insert error: {}", e); + } + }); + } + + fn insert_async_with_callback(&self, key: Self::Key, value: Self::Value, f: F) + where + F: FnOnce(Result) -> FU + Send + 'static, + FU: Future + Send + 'static, + { + let store = self.clone(); + tokio::spawn(async move { + let res = store.insert(key, value).await; + let future = f(res); + future.await; + }); + } + + fn insert_if_not_exists_async_with_callback(&self, key: Self::Key, value: Self::Value, f: F) + where + F: FnOnce(Result) -> FU + Send + 'static, + FU: Future + Send + 'static, + { + let store = self.clone(); + tokio::spawn(async move { + let res = store.insert_if_not_exists(key, value).await; + let future = f(res); + future.await; + }); + } +} + +impl AsyncStorageExt for S {} + +pub trait ForceStorageExt: Storage { + #[tracing::instrument(skip(self, value))] + fn insert_force(&self, key: Self::Key, value: Self::Value) -> impl Future> + Send { + let weight = key.serialized_len() + value.serialized_len(); + let mut writer = self.writer(key, weight); + writer.force(); + writer.finish(value) + } + + /// First judge if the entry will be admitted with `key` and `weight` by admission policies. + /// Then `f` will be called and entry will be inserted. + /// + /// # Safety + /// + /// `weight` MUST be equal to `key.serialized_len() + value.serialized_len()` + #[tracing::instrument(skip(self, f))] + fn insert_force_with(&self, key: Self::Key, f: F, weight: usize) -> impl Future> + Send + where + F: FnOnce() -> anyhow::Result + Send, + { + async move { + let mut writer = self.writer(key, weight); + writer.force(); + if !writer.judge() { + return Ok(false); + } + let value = match f() { + Ok(value) => value, + Err(e) => { + tracing::warn!("fetch value error: {:?}", e); + return Ok(false); + } + }; + let inserted = writer.finish(value).await?; + Ok(inserted) + } + } + + /// First judge if the entry will be admitted with `key` and `weight` by admission policies. + /// Then `f` will be called to fetch value, and entry will be inserted. + /// + /// # Safety + /// + /// `weight` MUST be equal to `key.serialized_len() + value.serialized_len()` + #[tracing::instrument(skip(self, f))] + fn insert_force_with_future( + &self, + key: Self::Key, + f: F, + weight: usize, + ) -> impl Future> + Send + where + F: FnOnce() -> FU + Send, + FU: FetchValueFuture, + { + async move { + let mut writer = self.writer(key, weight); + writer.force(); + if !writer.judge() { + return Ok(false); + } + let value = match f().await { + Ok(value) => value, + Err(e) => { + tracing::warn!("fetch value error: {:?}", e); + return Ok(false); + } + }; + let inserted = writer.finish(value).await?; + Ok(inserted) + } + } +} + +impl ForceStorageExt for S where S: Storage {} + +#[cfg(test)] +mod tests { + //! storage interface test + + use std::{path::Path, sync::Arc, time::Duration}; + + use foyer_intrusive::eviction::fifo::FifoConfig; + use tokio::sync::Barrier; + + use super::*; + use crate::{ + device::fs::FsDeviceConfig, + store::{FifoFsStore, FifoFsStoreConfig}, + }; + + const KB: usize = 1024; + const MB: usize = 1024 * 1024; + + fn config_for_test(dir: impl AsRef) -> FifoFsStoreConfig> { + FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: dir.as_ref().into(), + capacity: 4 * MB, + file_capacity: MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![], + reinsertions: vec![], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::None, + } + } + + #[tokio::test] + async fn test_storage() { + let tempdir = tempfile::tempdir().unwrap(); + let config = config_for_test(tempdir.path()); + + let storage = FifoFsStore::open(config).await.unwrap(); + assert!(storage.is_ready()); + + assert!(!storage.exists(&1).unwrap()); + + let mut writer = storage.writer(1, KB); + assert_eq!(writer.key(), &1); + assert_eq!(writer.weight(), KB); + assert!(writer.judge()); + assert_eq!(writer.compression(), Compression::None); + writer.set_compression(Compression::Lz4); + assert_eq!(writer.compression(), Compression::Lz4); + writer.force(); + assert!(writer.finish(vec![b'x'; KB]).await.unwrap()); + + assert!(storage.exists(&1).unwrap()); + assert_eq!(storage.lookup(&1).await.unwrap().unwrap(), vec![b'x'; KB]); + + assert!(storage.remove(&1).unwrap()); + assert!(!storage.exists(&1).unwrap()); + assert!(!storage.remove(&1).unwrap()); + + storage.clear().unwrap(); + storage.close().await.unwrap(); + } + + #[tokio::test] + async fn test_storage_ext() { + let tempdir = tempfile::tempdir().unwrap(); + let config = config_for_test(tempdir.path()); + + let storage = FifoFsStore::open(config).await.unwrap(); + + assert!(storage.insert(1, vec![b'x'; KB]).await.unwrap()); + assert!(storage.exists(&1).unwrap()); + + assert!(!storage.insert_if_not_exists(1, vec![b'x'; KB]).await.unwrap()); + assert!(storage.insert_if_not_exists(2, vec![b'x'; KB]).await.unwrap()); + assert!(storage.exists(&2).unwrap()); + + assert!(storage.insert_with(3, || { Ok(vec![b'x'; KB]) }, KB).await.unwrap()); + assert!(storage.exists(&3).unwrap()); + + assert!(storage + .insert_with_future(4, || { async move { Ok(vec![b'x'; KB]) } }, KB) + .await + .unwrap()); + assert!(storage.exists(&4).unwrap()); + + assert!(!storage + .insert_if_not_exists_with(4, || { Ok(vec![b'x'; KB]) }, KB) + .await + .unwrap()); + assert!(storage + .insert_if_not_exists_with(5, || { Ok(vec![b'x'; KB]) }, KB) + .await + .unwrap()); + assert!(storage.exists(&5).unwrap()); + + assert!(!storage + .insert_if_not_exists_with_future(5, || { async move { Ok(vec![b'x'; KB]) } }, KB) + .await + .unwrap()); + assert!(storage + .insert_if_not_exists_with_future(6, || { async move { Ok(vec![b'x'; KB]) } }, KB) + .await + .unwrap()); + assert!(storage.exists(&6).unwrap()); + } + + async fn exists_with_retry(storage: &impl Storage>, key: &u64) -> bool { + tokio::time::sleep(Duration::from_millis(1)).await; + for _ in 0..10 { + if storage.exists(key).unwrap() { + return true; + }; + tokio::time::sleep(Duration::from_millis(10)).await + } + false + } + + #[tokio::test] + async fn test_async_storage_ext() { + let tempdir = tempfile::tempdir().unwrap(); + let config = config_for_test(tempdir.path()); + + let storage = FifoFsStore::open(config).await.unwrap(); + + storage.insert_async(1, vec![b'x'; KB]); + assert!(exists_with_retry(&storage, &1).await); + + storage.insert_if_not_exists_async(2, vec![b'x'; KB]); + assert!(exists_with_retry(&storage, &2).await); + + let barrier = Arc::new(Barrier::new(2)); + let b = barrier.clone(); + storage.insert_async_with_callback(3, vec![b'x'; KB], |res| async move { + assert!(res.unwrap()); + b.wait().await; + }); + barrier.wait().await; + + storage.insert_if_not_exists_async_with_callback(3, vec![b'x'; KB], |res| async move { + assert!(!res.unwrap()); + }); + storage.insert_if_not_exists_async_with_callback(4, vec![b'x'; KB], |res| async move { + assert!(res.unwrap()); + }); + } +} diff --git a/foyer-storage/src/store.rs b/foyer-storage/src/store.rs new file mode 100644 index 00000000..0d5b8f5f --- /dev/null +++ b/foyer-storage/src/store.rs @@ -0,0 +1,452 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::marker::PhantomData; + +use foyer_common::code::{Key, Value}; +use foyer_intrusive::eviction::{ + fifo::{Fifo, FifoLink}, + lfu::{Lfu, LfuLink}, + lru::{Lru, LruLink}, +}; + +use crate::{ + compress::Compression, + device::fs::FsDevice, + error::Result, + generic::{GenericStore, GenericStoreConfig, GenericStoreWriter}, + region_manager::RegionEpItemAdapter, + storage::{Storage, StorageWriter}, +}; + +pub type LruFsStore = GenericStore>, LruLink>; + +pub type LruFsStoreConfig = GenericStoreConfig>>; + +pub type LruFsStoreWriter = GenericStoreWriter>, LruLink>; + +pub type LfuFsStore = GenericStore>, LfuLink>; + +pub type LfuFsStoreConfig = GenericStoreConfig>>; + +pub type LfuFsStoreWriter = GenericStoreWriter>, LfuLink>; + +pub type FifoFsStore = GenericStore>, FifoLink>; + +pub type FifoFsStoreConfig = GenericStoreConfig>>; + +pub type FifoFsStoreWriter = GenericStoreWriter>, FifoLink>; + +#[derive(Debug)] +pub struct NoneStoreWriter { + key: K, + weight: usize, + _marker: PhantomData, +} + +impl NoneStoreWriter { + pub fn new(key: K, weight: usize) -> Self { + Self { + key, + weight, + _marker: PhantomData, + } + } +} + +impl StorageWriter for NoneStoreWriter { + type Key = K; + type Value = V; + + fn key(&self) -> &Self::Key { + &self.key + } + + fn weight(&self) -> usize { + self.weight + } + + fn judge(&mut self) -> bool { + false + } + + fn force(&mut self) {} + + async fn finish(self, _: Self::Value) -> Result { + Ok(false) + } + + fn compression(&self) -> Compression { + Compression::None + } + + fn set_compression(&mut self, _: Compression) {} +} + +#[derive(Debug)] +pub struct NoneStore(PhantomData<(K, V)>); + +impl Default for NoneStore { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Clone for NoneStore { + fn clone(&self) -> Self { + Self(PhantomData) + } +} + +impl Storage for NoneStore { + type Key = K; + type Value = V; + type Config = (); + type Writer = NoneStoreWriter; + + async fn open(_: Self::Config) -> Result { + Ok(NoneStore(PhantomData)) + } + + fn is_ready(&self) -> bool { + true + } + + async fn close(&self) -> Result<()> { + Ok(()) + } + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer { + NoneStoreWriter::new(key, weight) + } + + fn exists(&self, _: &Self::Key) -> Result { + Ok(false) + } + + async fn lookup(&self, _: &Self::Key) -> Result> { + Ok(None) + } + + fn remove(&self, _: &Self::Key) -> Result { + Ok(false) + } + + fn clear(&self) -> Result<()> { + Ok(()) + } +} + +#[derive(Debug)] +pub enum StoreConfig +where + K: Key, + V: Value, +{ + LruFsStoreConfig { config: LruFsStoreConfig }, + LfuFsStoreConfig { config: LfuFsStoreConfig }, + FifoFsStoreConfig { config: FifoFsStoreConfig }, + NoneStoreConfig, +} + +impl Clone for StoreConfig +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + match self { + Self::LruFsStoreConfig { config } => Self::LruFsStoreConfig { config: config.clone() }, + Self::LfuFsStoreConfig { config } => Self::LfuFsStoreConfig { config: config.clone() }, + Self::FifoFsStoreConfig { config } => Self::FifoFsStoreConfig { config: config.clone() }, + Self::NoneStoreConfig => Self::NoneStoreConfig, + } + } +} + +impl From> for StoreConfig +where + K: Key, + V: Value, +{ + fn from(config: LruFsStoreConfig) -> Self { + StoreConfig::LruFsStoreConfig { config } + } +} + +impl From> for StoreConfig +where + K: Key, + V: Value, +{ + fn from(config: LfuFsStoreConfig) -> Self { + StoreConfig::LfuFsStoreConfig { config } + } +} + +impl From> for StoreConfig +where + K: Key, + V: Value, +{ + fn from(config: FifoFsStoreConfig) -> Self { + StoreConfig::FifoFsStoreConfig { config } + } +} + +#[derive(Debug)] +pub enum StoreWriter +where + K: Key, + V: Value, +{ + LruFsStorWriter { writer: LruFsStoreWriter }, + LfuFsStorWriter { writer: LfuFsStoreWriter }, + FifoFsStoreWriter { writer: FifoFsStoreWriter }, + NoneStoreWriter { writer: NoneStoreWriter }, +} + +impl From> for StoreWriter +where + K: Key, + V: Value, +{ + fn from(writer: LruFsStoreWriter) -> Self { + StoreWriter::LruFsStorWriter { writer } + } +} + +impl From> for StoreWriter +where + K: Key, + V: Value, +{ + fn from(writer: LfuFsStoreWriter) -> Self { + StoreWriter::LfuFsStorWriter { writer } + } +} + +impl From> for StoreWriter +where + K: Key, + V: Value, +{ + fn from(writer: FifoFsStoreWriter) -> Self { + StoreWriter::FifoFsStoreWriter { writer } + } +} + +impl From> for StoreWriter +where + K: Key, + V: Value, +{ + fn from(writer: NoneStoreWriter) -> Self { + StoreWriter::NoneStoreWriter { writer } + } +} + +#[derive(Debug)] +pub enum Store +where + K: Key, + V: Value, +{ + LruFsStore { store: LruFsStore }, + LfuFsStore { store: LfuFsStore }, + FifoFsStore { store: FifoFsStore }, + NoneStore { store: NoneStore }, +} + +impl Clone for Store +where + K: Key, + V: Value, +{ + fn clone(&self) -> Self { + match self { + Self::LruFsStore { store } => Self::LruFsStore { store: store.clone() }, + Self::LfuFsStore { store } => Self::LfuFsStore { store: store.clone() }, + Self::FifoFsStore { store } => Self::FifoFsStore { store: store.clone() }, + Self::NoneStore { store } => Self::NoneStore { store: store.clone() }, + } + } +} + +impl StorageWriter for StoreWriter +where + K: Key, + V: Value, +{ + type Key = K; + type Value = V; + + fn key(&self) -> &Self::Key { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.key(), + StoreWriter::LfuFsStorWriter { writer } => writer.key(), + StoreWriter::FifoFsStoreWriter { writer } => writer.key(), + StoreWriter::NoneStoreWriter { writer } => writer.key(), + } + } + + fn weight(&self) -> usize { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.weight(), + StoreWriter::LfuFsStorWriter { writer } => writer.weight(), + StoreWriter::FifoFsStoreWriter { writer } => writer.weight(), + StoreWriter::NoneStoreWriter { writer } => writer.weight(), + } + } + + fn judge(&mut self) -> bool { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.judge(), + StoreWriter::LfuFsStorWriter { writer } => writer.judge(), + StoreWriter::FifoFsStoreWriter { writer } => writer.judge(), + StoreWriter::NoneStoreWriter { writer } => writer.judge(), + } + } + + fn force(&mut self) { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.force(), + StoreWriter::LfuFsStorWriter { writer } => writer.force(), + StoreWriter::FifoFsStoreWriter { writer } => writer.force(), + StoreWriter::NoneStoreWriter { writer } => writer.force(), + } + } + + async fn finish(self, value: Self::Value) -> Result { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.finish(value).await, + StoreWriter::LfuFsStorWriter { writer } => writer.finish(value).await, + StoreWriter::FifoFsStoreWriter { writer } => writer.finish(value).await, + StoreWriter::NoneStoreWriter { writer } => writer.finish(value).await, + } + } + + fn compression(&self) -> Compression { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.compression(), + StoreWriter::LfuFsStorWriter { writer } => writer.compression(), + StoreWriter::FifoFsStoreWriter { writer } => writer.compression(), + StoreWriter::NoneStoreWriter { writer } => writer.compression(), + } + } + + fn set_compression(&mut self, compression: Compression) { + match self { + StoreWriter::LruFsStorWriter { writer } => writer.set_compression(compression), + StoreWriter::LfuFsStorWriter { writer } => writer.set_compression(compression), + StoreWriter::FifoFsStoreWriter { writer } => writer.set_compression(compression), + StoreWriter::NoneStoreWriter { writer } => writer.set_compression(compression), + } + } +} + +impl Storage for Store +where + K: Key, + V: Value, +{ + type Key = K; + type Value = V; + type Config = StoreConfig; + type Writer = StoreWriter; + + async fn open(config: Self::Config) -> Result { + match config { + StoreConfig::LruFsStoreConfig { config } => { + let store = LruFsStore::open(config).await?; + Ok(Self::LruFsStore { store }) + } + StoreConfig::LfuFsStoreConfig { config } => { + let store = LfuFsStore::open(config).await?; + Ok(Self::LfuFsStore { store }) + } + StoreConfig::FifoFsStoreConfig { config } => { + let store = FifoFsStore::open(config).await?; + Ok(Self::FifoFsStore { store }) + } + StoreConfig::NoneStoreConfig => { + let store = NoneStore::open(()).await?; + Ok(Self::NoneStore { store }) + } + } + } + + fn is_ready(&self) -> bool { + match self { + Store::LruFsStore { store } => store.is_ready(), + Store::LfuFsStore { store } => store.is_ready(), + Store::FifoFsStore { store } => store.is_ready(), + Store::NoneStore { store } => store.is_ready(), + } + } + + async fn close(&self) -> Result<()> { + match self { + Store::LruFsStore { store } => store.close().await, + Store::LfuFsStore { store } => store.close().await, + Store::FifoFsStore { store } => store.close().await, + Store::NoneStore { store } => store.close().await, + } + } + + fn writer(&self, key: Self::Key, weight: usize) -> Self::Writer { + match self { + Store::LruFsStore { store } => store.writer(key, weight).into(), + Store::LfuFsStore { store } => store.writer(key, weight).into(), + Store::FifoFsStore { store } => store.writer(key, weight).into(), + Store::NoneStore { store } => store.writer(key, weight).into(), + } + } + + fn exists(&self, key: &Self::Key) -> Result { + match self { + Store::LruFsStore { store } => store.exists(key), + Store::LfuFsStore { store } => store.exists(key), + Store::FifoFsStore { store } => store.exists(key), + Store::NoneStore { store } => store.exists(key), + } + } + + async fn lookup(&self, key: &Self::Key) -> Result> { + match self { + Store::LruFsStore { store } => store.lookup(key).await, + Store::LfuFsStore { store } => store.lookup(key).await, + Store::FifoFsStore { store } => store.lookup(key).await, + Store::NoneStore { store } => store.lookup(key).await, + } + } + + fn remove(&self, key: &Self::Key) -> Result { + match self { + Store::LruFsStore { store } => store.remove(key), + Store::LfuFsStore { store } => store.remove(key), + Store::FifoFsStore { store } => store.remove(key), + Store::NoneStore { store } => store.remove(key), + } + } + + fn clear(&self) -> Result<()> { + match self { + Store::LruFsStore { store } => store.clear(), + Store::LfuFsStore { store } => store.clear(), + Store::FifoFsStore { store } => store.clear(), + Store::NoneStore { store } => store.clear(), + } + } +} diff --git a/foyer-storage/src/test_utils.rs b/foyer-storage/src/test_utils.rs new file mode 100644 index 00000000..1c155ca0 --- /dev/null +++ b/foyer-storage/src/test_utils.rs @@ -0,0 +1,120 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.s + +use std::{collections::HashSet, marker::PhantomData}; + +use foyer_common::code::{Key, Value}; +use parking_lot::Mutex; + +use crate::{ + admission::{AdmissionContext, AdmissionPolicy}, + reinsertion::{ReinsertionContext, ReinsertionPolicy}, +}; + +#[derive(Debug, Clone)] +pub enum Record { + Admit(K), + Evict(K), +} + +#[derive(Debug)] +pub struct JudgeRecorder +where + K: Key, + V: Value, +{ + records: Mutex>>, + _marker: PhantomData, +} + +impl JudgeRecorder +where + K: Key, + V: Value, +{ + pub fn dump(&self) -> Vec> { + self.records.lock().clone() + } + + pub fn remains(&self) -> HashSet { + let records = self.dump(); + let mut res = HashSet::default(); + for record in records { + match record { + Record::Admit(key) => { + res.insert(key); + } + Record::Evict(key) => { + res.remove(&key); + } + } + } + res + } +} + +impl Default for JudgeRecorder +where + K: Key, + V: Value, +{ + fn default() -> Self { + Self { + records: Mutex::new(Vec::default()), + _marker: PhantomData, + } + } +} + +impl AdmissionPolicy for JudgeRecorder +where + K: Key, + V: Value, +{ + type Key = K; + + type Value = V; + + fn init(&self, _: AdmissionContext) {} + + fn judge(&self, key: &K, _weight: usize) -> bool { + self.records.lock().push(Record::Admit(key.clone())); + true + } + + fn on_insert(&self, _key: &K, _weight: usize, _judge: bool) {} + + fn on_drop(&self, _key: &K, _weight: usize, _judge: bool) {} +} + +impl ReinsertionPolicy for JudgeRecorder +where + K: Key, + V: Value, +{ + type Key = K; + + type Value = V; + + fn init(&self, _: ReinsertionContext) {} + + fn judge(&self, key: &K, _weight: usize) -> bool { + self.records.lock().push(Record::Evict(key.clone())); + false + } + + fn on_insert(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} + + fn on_drop(&self, _key: &Self::Key, _weight: usize, _judge: bool) {} +} diff --git a/foyer-storage/tests/storage_test.rs b/foyer-storage/tests/storage_test.rs new file mode 100644 index 00000000..3910f881 --- /dev/null +++ b/foyer-storage/tests/storage_test.rs @@ -0,0 +1,278 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO(MrCroxx): use `expect` after `lint_reasons` is stable. +#![allow(clippy::identity_op)] + +use std::{path::PathBuf, sync::Arc, time::Duration}; + +use foyer_intrusive::eviction::fifo::FifoConfig; +use foyer_storage::{ + compress::Compression, + device::fs::FsDeviceConfig, + lazy::LazyStore, + runtime::{RuntimeConfig, RuntimeLazyStore, RuntimeStorageConfig, RuntimeStore}, + storage::{Storage, StorageExt}, + store::{FifoFsStoreConfig, Store}, + test_utils::JudgeRecorder, +}; + +const KB: usize = 1024; +const MB: usize = 1024 * 1024; + +const INSERTS: usize = 100; +const LOOPS: usize = 10; + +async fn test_storage(config: S::Config, recorder: Arc>>) +where + S: Storage>, +{ + let store = S::open(config.clone()).await.unwrap(); + while !store.is_ready() { + tokio::time::sleep(Duration::from_millis(10)).await; + } + + let mut index = 0; + + for _ in 0..INSERTS as u64 { + index += 1; + store.insert(index, vec![index as u8; 1 * KB]).await.unwrap(); + } + + store.close().await.unwrap(); + + let remains = recorder.remains(); + + for i in 0..INSERTS as u64 * (LOOPS + 1) as u64 { + if remains.contains(&i) { + assert_eq!(store.lookup(&i).await.unwrap().unwrap(), vec![i as u8; 1 * KB],); + } else { + assert!(store.lookup(&i).await.unwrap().is_none()); + } + } + + drop(store); + + for _ in 0..LOOPS { + let store = S::open(config.clone()).await.unwrap(); + while !store.is_ready() { + tokio::time::sleep(Duration::from_millis(10)).await; + } + + let remains = recorder.remains(); + + for i in 0..INSERTS as u64 * (LOOPS + 1) as u64 { + if remains.contains(&i) { + assert_eq!(store.lookup(&i).await.unwrap().unwrap(), vec![i as u8; 1 * KB],); + } else { + assert!(store.lookup(&i).await.unwrap().is_none()); + } + } + + for _ in 0..INSERTS as u64 { + index += 1; + store.insert(index, vec![index as u8; 1 * KB]).await.unwrap(); + } + + store.close().await.unwrap(); + + let remains = recorder.remains(); + + for i in 0..INSERTS as u64 * (LOOPS + 1) as u64 { + if remains.contains(&i) { + assert_eq!(store.lookup(&i).await.unwrap().unwrap(), vec![i as u8; 1 * KB],); + } else { + assert!(store.lookup(&i).await.unwrap().is_none()); + } + } + + drop(store); + } +} + +#[tokio::test] +async fn test_store() { + let tempdir = tempfile::tempdir().unwrap(); + let recorder = Arc::new(JudgeRecorder::default()); + let config = FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 4 * MB, + file_capacity: 1 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![recorder.clone()], + reinsertions: vec![recorder.clone()], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::None, + }; + + test_storage::>(config.into(), recorder).await; +} + +#[tokio::test] +async fn test_store_zstd() { + let tempdir = tempfile::tempdir().unwrap(); + let recorder = Arc::new(JudgeRecorder::default()); + let config = FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 4 * MB, + file_capacity: 1 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![recorder.clone()], + reinsertions: vec![recorder.clone()], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::Zstd, + }; + + test_storage::>(config.into(), recorder).await; +} + +#[tokio::test] +async fn test_store_lz4() { + let tempdir = tempfile::tempdir().unwrap(); + let recorder = Arc::new(JudgeRecorder::default()); + let config = FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 4 * MB, + file_capacity: 1 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![recorder.clone()], + reinsertions: vec![recorder.clone()], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::Lz4, + }; + + test_storage::>(config.into(), recorder).await; +} + +#[tokio::test] +async fn test_lazy_store() { + let tempdir = tempfile::tempdir().unwrap(); + let recorder = Arc::new(JudgeRecorder::default()); + let config = FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 4 * MB, + file_capacity: 1 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![recorder.clone()], + reinsertions: vec![recorder.clone()], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::None, + }; + + test_storage::>(config.into(), recorder).await; +} + +#[tokio::test] +async fn test_runtime_store() { + let tempdir = tempfile::tempdir().unwrap(); + let recorder = Arc::new(JudgeRecorder::default()); + let config = RuntimeStorageConfig { + store: FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 4 * MB, + file_capacity: 1 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![recorder.clone()], + reinsertions: vec![recorder.clone()], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::None, + } + .into(), + runtime: RuntimeConfig { + worker_threads: None, + thread_name: None, + }, + }; + + test_storage::>(config, recorder).await; +} + +#[tokio::test] +async fn test_runtime_lazy_store() { + let tempdir = tempfile::tempdir().unwrap(); + let recorder = Arc::new(JudgeRecorder::default()); + let config = RuntimeStorageConfig { + store: FifoFsStoreConfig { + name: "".to_string(), + eviction_config: FifoConfig, + device_config: FsDeviceConfig { + dir: PathBuf::from(tempdir.path()), + capacity: 4 * MB, + file_capacity: 1 * MB, + align: 4 * KB, + io_size: 4 * KB, + }, + catalog_bits: 1, + admissions: vec![recorder.clone()], + reinsertions: vec![recorder.clone()], + flushers: 1, + reclaimers: 1, + clean_region_threshold: 1, + recover_concurrency: 2, + compression: Compression::None, + } + .into(), + runtime: RuntimeConfig { + worker_threads: None, + thread_name: None, + }, + }; + + test_storage::>(config, recorder).await; +} diff --git a/foyer-workspace-hack/.gitattributes b/foyer-workspace-hack/.gitattributes new file mode 100644 index 00000000..3e9dba4b --- /dev/null +++ b/foyer-workspace-hack/.gitattributes @@ -0,0 +1,4 @@ +# Avoid putting conflict markers in the generated Cargo.toml file, since their presence breaks +# Cargo. +# Also do not check out the file as CRLF on Windows, as that's what hakari needs. +Cargo.toml merge=binary -crlf diff --git a/foyer-workspace-hack/Cargo.toml b/foyer-workspace-hack/Cargo.toml new file mode 100644 index 00000000..9ad39a48 --- /dev/null +++ b/foyer-workspace-hack/Cargo.toml @@ -0,0 +1,48 @@ +# This file is generated by `cargo hakari`. +# To regenerate, run: +# cargo hakari generate + +[package] +name = "foyer-workspace-hack" +version = "0.4.0" +authors = ["MrCroxx "] +description = "workspace-hack package, managed by hakari" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +# You can choose to publish this crate: see https://docs.rs/cargo-hakari/latest/cargo_hakari/publishing. +publish = true +# The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments +# are managed by hakari. + +### BEGIN HAKARI SECTION +[dependencies] +ahash = { version = "0.8" } +allocator-api2 = { version = "0.2" } +crossbeam-channel = { version = "0.5" } +crossbeam-epoch = { version = "0.9" } +crossbeam-utils = { version = "0.8" } +either = { version = "1", default-features = false, features = ["use_std"] } +futures-channel = { version = "0.3", features = ["sink"] } +futures-core = { version = "0.3" } +futures-executor = { version = "0.3" } +futures-sink = { version = "0.3" } +futures-util = { version = "0.3", default-features = false, features = ["async-await-macro", "channel", "io", "sink"] } +hashbrown = { version = "0.14", features = ["raw"] } +libc = { version = "0.2", features = ["extra_traits"] } +parking_lot = { version = "0.12", features = ["arc_lock", "deadlock_detection"] } +parking_lot_core = { version = "0.9", default-features = false, features = ["deadlock_detection"] } +rand = { version = "0.8", features = ["small_rng"] } +smallvec = { version = "1", default-features = false, features = ["const_new"] } +tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time", "tracing"] } +tracing-core = { version = "0.1" } + +[build-dependencies] +cc = { version = "1", default-features = false, features = ["parallel"] } +either = { version = "1", default-features = false, features = ["use_std"] } +proc-macro2 = { version = "1" } +quote = { version = "1" } +syn = { version = "2", features = ["extra-traits", "full", "visit-mut"] } + +### END HAKARI SECTION diff --git a/foyer-workspace-hack/build.rs b/foyer-workspace-hack/build.rs new file mode 100644 index 00000000..91682caf --- /dev/null +++ b/foyer-workspace-hack/build.rs @@ -0,0 +1,16 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A build script is required for cargo to consider build dependencies. +fn main() {} diff --git a/src/lib.rs b/foyer-workspace-hack/src/lib.rs similarity index 89% rename from src/lib.rs rename to foyer-workspace-hack/src/lib.rs index e27f3cba..2b5d0f4b 100644 --- a/src/lib.rs +++ b/foyer-workspace-hack/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2023 MrCroxx +// Copyright 2024 Foyer Project Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,4 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod utils; +// This is a stub lib.rs. diff --git a/foyer/Cargo.toml b/foyer/Cargo.toml new file mode 100644 index 00000000..5e3652c2 --- /dev/null +++ b/foyer/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "foyer" +version = "0.7.0" +edition = "2021" +authors = ["MrCroxx "] +description = "Hybrid cache for Rust" +license = "Apache-2.0" +repository = "https://github.com/mrcroxx/foyer" +homepage = "https://github.com/mrcroxx/foyer" +readme = "../README.md" +rust-version = "1.77.2" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package.metadata.cargo-udeps.ignore] +normal = ["foyer-workspace-hack"] + +[dependencies] +foyer-common = { version = "0.5", path = "../foyer-common" } +foyer-intrusive = { version = "0.4", path = "../foyer-intrusive" } +foyer-memory = { version = "0.2", path = "../foyer-memory" } +foyer-storage = { version = "0.6", path = "../foyer-storage" } +foyer-workspace-hack = { version = "0.4", path = "../foyer-workspace-hack" } diff --git a/foyer/src/lib.rs b/foyer/src/lib.rs new file mode 100644 index 00000000..32d97e4d --- /dev/null +++ b/foyer/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2024 Foyer Project Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use foyer_common as common; +pub use foyer_intrusive as intrusive; +pub use foyer_memory as memory; +pub use foyer_storage as storage; diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index ab84e227..00000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly-2023-04-07 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..16bafd46 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +# TODO(MrCroxx): Restore comments after the features are stable. + +# imports_granularity = "Crate" +# group_imports = "StdExternalCrate" +tab_spaces = 4 +# wrap_comments = true +max_width = 120 +# comment_width = 120 diff --git a/scripts/install-deps.sh b/scripts/install-deps.sh new file mode 100755 index 00000000..31641bc4 --- /dev/null +++ b/scripts/install-deps.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ -z "$(which cargo-binstall)" ]; then + curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash +fi + +cargo binstall -y cargo-hakari cargo-sort cargo-nextest typos-cli \ No newline at end of file diff --git a/scripts/minimize-dashboards.sh b/scripts/minimize-dashboards.sh new file mode 100755 index 00000000..e2d6b973 --- /dev/null +++ b/scripts/minimize-dashboards.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# You will need to install jq to use this tool. +# brew install jq + +set -e + +DIR="etc/grafana/dashboards" + +for dashboard in "${DIR}"/*.json; do + if [[ "$dashboard" == *.min.json ]]; then + continue + fi + name=$(basename "$dashboard" .json) + jq -c < "$dashboard" > "${DIR}/${name}.min.json" +done + +for dashboard in "${DIR}"/*.min.json; do + name=$(basename "$dashboard" .min.json) + mv "$dashboard" "${DIR}/${name}.json" +done + +if [ "$1" == "--check" ] ; then + if ! git diff --exit-code; then + echo "Please run minimize-dashboards.sh and commit after editing the grafana dashboards." + exit 1 + fi +fi \ No newline at end of file diff --git a/scripts/monitor.sh b/scripts/monitor.sh new file mode 100755 index 00000000..23b9c9de --- /dev/null +++ b/scripts/monitor.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +compose=$(docker compose ps -q | wc -l) + +cat < docker-compose.override.yaml +version: '3' + +services: + prometheus: + user: "${UID}" +EOF + +if [ "$compose" = 0 ] ; then + mkdir -p .tmp/prometheus + docker compose up -d +else + docker compose down +fi \ No newline at end of file diff --git a/src/utils/count_min_sketch.rs b/src/utils/count_min_sketch.rs deleted file mode 100644 index 1bda3d03..00000000 --- a/src/utils/count_min_sketch.rs +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright 2023 MrCroxx -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Copyright (c) Meta Platforms, Inc. and affiliates. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! A probabilistic counting data structure that never undercounts items before -//! it hits counter's capacity. It is a table structure with the depth being the -//! number of hashes and the width being the number of unique items. When a key -//! is inserted, each row's hash function is used to generate the index for that -//! row. Then the element's count at that index is incremented. As a result, one -//! key being inserted will increment different indicies in each row. Querying the -//! count returns the minimum values of these elements since some hashes might collide. -//! -//! -//! Users are supposed to synchronize concurrent accesses to the data structure. -//! -//! E.g. insert(1) -//! hash1(1) = 2 -> increment row 1, index 2 -//! hash2(1) = 5 -> increment row 2, index 5 -//! hash3(1) = 3 -> increment row 3, index 3 -//! etc. - -use crate::utils::hash::{combine_hashes, twang_mix64}; -use paste::paste; - -macro_rules! for_all_uint_types { - ($macro:ident) => { - $macro! { - {u8, U8}, - {u16, U16}, - {u32, U32}, - {u64, U64}, - {usize, Usize}, - } - }; -} - -macro_rules! cm_sketch { - ($( {$type:ty, $suffix:ident}, )*) => { - paste! { - $( - pub struct [] { - width: usize, - depth: usize, - saturated: usize, - - /// size = width * depth - table: Vec<$type> - } - - impl [] { - - /// `error`: Tolerable error in count given as a fraction of the total number of inserts. Must be between 0 and 1. - /// `probability`: The certainty that the count is with in the error threshold. - /// `max_width`: Maximum number of the elements per row in the table. 0 represents there is no limitations. - /// `max_depth`: Maximum num of rows. 0 represents there is no limitations. - pub fn new(error: f64, probability: f64, max_width: usize, max_depth: usize) -> Self { - let width = Self::calculate_width(error, max_width); - let depth = Self::calculate_depth(probability, max_depth); - - Self::new_with_size(width, depth) - } - - pub fn new_with_size(width: usize, depth: usize) -> Self { - assert!(width > 0, "Width must be greater than 0, width: {}.", width); - assert!(depth > 0, "Depth must be greater than 0, depth: {}.", depth); - - Self { - width, - depth, - saturated: 0, - - table: vec![0; width * depth], - } - } - - pub fn add(&mut self, key: u64) { - for i in 0..self.depth { - let index = self.index(i, key); - if self.table[index] < self.max_count() { - self.table[index] += 1; - if self.table[index] == self.max_count() { - self.saturated += 1; - } - } - // println!("(ins) table[{}, {}, {}] = {}", i, index, key, self.table[index]); - } - } - - pub fn count(&self, key: u64) -> $type { - (0..self.depth) - .into_iter() - .map(|i| self.table[self.index(i, key)]) - // .map(|i| { - // let index = self.index(i, key); - // let res = self.table[index]; - // println!("(cnt) table[{}, {}, {}] = {}", i, index, key, res); - // res - // }) - .min() - .unwrap() - } - - pub fn remove(&mut self, key: u64) { - let count = self.count(key); - for i in 0..self.depth { - let index = self.index(i, key); - self.table[index] -= count; - } - } - - pub fn clear(&mut self) { - self.saturated = 0; - self.table.iter_mut().for_each(|c| *c = 0); - } - - pub fn decay(&mut self, decay: f64) { - self.table.iter_mut().for_each(|c| *c = (*c as f64 * decay) as $type); - } - - pub fn width(&self) -> usize { - self.width - } - - pub fn depth(&self) -> usize { - self.depth - } - - pub fn max_count(&self) -> $type { - $type::MAX - } - - pub fn saturated(&self) -> usize { - self.saturated - } - - /// `max_width == 0` represents there is no limitations. - fn calculate_width(error: f64, max_width: usize) -> usize { - assert!(error > 0.0 && error < 1.0, "Error should be greater than 0 and less than 1, error: {}.", error); - - // From "Approximating Data with the Count-Min Data Structure" (Cormode & Muthukrishnan) - let width = (2.0 / error).ceil() as usize; - if max_width > 0 { - std::cmp::min(width, max_width) - }else { - width - } - } - - /// `max_depth == 0` represents there is no limitations. - fn calculate_depth(probability: f64, max_depth: usize) -> usize { - assert!(probability > 0.0 && probability < 1.0, "Probability should be greater than 0 and less than 1, probability: {}.", probability); - - // From "Approximating Data with the Count-Min Data Structure" (Cormode & Muthukrishnan) - let depth = (1.0 - probability).log2().abs().ceil() as usize; - let depth = std::cmp::max(1, depth); - if max_depth > 0 { - std::cmp::min(depth, max_depth) - } else { - depth - } - } - - fn index(&self, hash_index: usize, key: u64) -> usize { - hash_index as usize * self.width - + (combine_hashes(twang_mix64(hash_index as u64), key) as usize % self.width) - } - } - )* - } - }; -} - -for_all_uint_types! {cm_sketch} - -#[cfg(test)] -mod tests { - use super::*; - use rand_mt::Mt64; - - macro_rules! test_cm_sketch { - ($( {$type:ty, $suffix:ident}, )*) => { - paste! { - $( - #[test] - fn []() { - let mut cms = []::new_with_size(100, 3); - let mut rng = Mt64::new_unseeded(); - let mut keys = vec![]; - - for i in 0..10 { - keys.push(rng.next_u64()); - for _ in 0..i { - cms.add(keys[i]); - } - } - - for i in 0..10 { - assert!( - cms.count(keys[i]) >= std::cmp::min(i as $type, cms.max_count()), - "assert {} >= {} failed", - cms.count(keys[i]), std::cmp::min(i as $type, cms.max_count()) - ); - } - } - - #[test] - fn []() { - let mut cms = []::new_with_size(100, 3); - let mut rng = Mt64::new_unseeded(); - let mut keys = vec![]; - - for i in 0..10 { - keys.push(rng.next_u64()); - for _ in 0..i { - cms.add(keys[i]); - } - } - - for i in 0..10 { - cms.remove(keys[i]); - assert_eq!(cms.count(keys[i]), 0); - } - } - - #[test] - fn []() { - let mut cms = []::new_with_size(100, 3); - let mut rng = Mt64::new_unseeded(); - let mut keys = vec![]; - - for i in 0..10 { - keys.push(rng.next_u64()); - for _ in 0..i { - cms.add(keys[i]); - } - } - - cms.clear(); - for i in 0..10 { - assert_eq!(cms.count(keys[i]), 0); - } - } - - #[test] - fn []() { - let mut cms = []::new_with_size(40, 5); - let mut rng = Mt64::new_unseeded(); - let mut keys = vec![]; - let mut sum = 0; - - // Try inserting more keys than cms table width - for i in 0..55 { - keys.push(rng.next_u64()); - for _ in 0..i { - cms.add(keys[i]); - } - sum += i; - } - - let error = sum as f64 * 0.05; - for i in 0..10 { - assert!(cms.count(keys[i]) >= i as $type); - assert!(i as f64 + error >= cms.count(keys[i]) as f64); - } - } - - #[test] - fn []() { - let cms = []::new(0.01, 0.95, 0, 0); - assert_eq!(cms.width(), 200); - assert_eq!(cms.depth(), 5); - } - - - #[test] - #[should_panic] - fn []() { - []::new(0f64, 0f64, 0, 0); - } - - #[test] - fn []() { - let mut cms = []::new_with_size(100, 3); - let mut rng = Mt64::new_unseeded(); - let mut keys = vec![]; - - for i in 0..1000 { - keys.push(rng.next_u64()); - for _ in 0..i { - cms.add(keys[i]); - } - } - - for i in 0..1000 { - assert!( - cms.count(keys[i]) >= std::cmp::min(i as $type, cms.max_count()), - "assert {} >= {} failed", - cms.count(keys[i]), std::cmp::min(i as $type, cms.max_count()) - ); - } - - const FACTOR: f64 = 0.5; - cms.decay(FACTOR); - - for i in 0..1000 { - assert!(cms.count(keys[i]) >= (std::cmp::min(i as $type, cms.max_count()) as f64 * FACTOR).floor() as $type); - } - } - - #[test] - fn []() { - let mut cms = []::new_with_size(10, 3); - let mut rng = Mt64::new_unseeded(); - let key = rng.next_u64(); - let max = cms.max_count(); - - // Skip test if max is too large. - if (max as usize) < (u32::MAX as usize) { - for _ in 0..max { - cms.add(key); - } - - assert_eq!(cms.count(key), max); - assert_eq!(cms.saturated(), 3); - } - } - )* - } - }; - } - - for_all_uint_types! {test_cm_sketch} -} diff --git a/src/utils/hash.rs b/src/utils/hash.rs deleted file mode 100644 index a68a0f8d..00000000 --- a/src/utils/hash.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2023 MrCroxx -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Copyright (c) Meta Platforms, Inc. and affiliates. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Reduce two 64-bit hashes into one. -/// -/// Ported from CacheLib, which uses the `Hash128to64` function from Google's city hash. -pub fn combine_hashes(upper: u64, lower: u64) -> u64 { - const MUL: u64 = 0x9ddfea08eb382d69; - - let mut a = (lower ^ upper).wrapping_mul(MUL); - a ^= a >> 47; - let mut b = (upper ^ a).wrapping_mul(MUL); - b ^= b >> 47; - b = b.wrapping_mul(MUL); - b -} - -pub fn twang_mix64(val: u64) -> u64 { - let mut val = (!val).wrapping_add(val << 21); // val *= (1 << 21); val -= 1 - val = val ^ (val >> 24); - val = val.wrapping_add(val << 3).wrapping_add(val << 8); // val *= 1 + (1 << 3) + (1 << 8) - val = val ^ (val >> 14); - val = val.wrapping_add(val << 2).wrapping_add(val << 4); // va; *= 1 + (1 << 2) + (1 << 4) - val = val ^ (val >> 28); - val = val.wrapping_add(val << 31); // val *= 1 + (1 << 31) - val -}