New Upstream Release - ruby-prometheus-client-mmap

Ready changes

Summary

Merged new upstream version: 0.24.3 (was: 0.19.1).

Diff

diff --git a/.gitignore b/.gitignore
index 5e00db0..d09a7a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@ Gemfile.lock
 pkg/
 prometheus-client-mmap*.gem
 tmp/
-lib/fast_mmaped_file.bundle
+lib/*.bundle
+lib/*.so
 ext/fast_mmaped_file/jsmn.c
-ext/fast_mmaped_file/hashmap.c
\ No newline at end of file
+ext/fast_mmaped_file/hashmap.c
+ext/fast_mmaped_file_rs/target
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9599575..4ca4655 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,67 +1,107 @@
+stages:
+  - test
+  - build_gems
+  - smoke_test
+  - deploy
+  - release
+
+.install-rust: &install-rust
+  - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet --default-toolchain=1.65.0 --profile=minimal
+  - source "$HOME/.cargo/env"
+
+.install-ruby: &install-ruby
+  - ruby -v
+  - gem install bundler --no-document
+  - bundle --version
+  - bundle config --local path vendor
+  - bundle install -j $(nproc)
+
+.install-ruby-and-compile: &install-ruby-and-compile
+  - *install-ruby
+  - bundle exec rake compile
+
+.test-ruby: &test-ruby
+  - bundle exec rake spec
+  - prometheus_rust_mmaped_file=true bundle exec rake spec
+
+cache:
+  key: ${CI_JOB_IMAGE}
+  paths:
+    - vendor/ruby
+
+before_script:
+  - apt-get update
+  - apt-get install -y curl ruby ruby-dev build-essential llvm-dev libclang-dev clang
+  - *install-rust
+  - *install-ruby-and-compile
+
 .test-job: &test-job
   image: ruby:${RUBY_VERSION}
   variables:
     prometheus_multiproc_dir: tmp/
-    BUILDER_IMAGE_REVISION: "2.12.0"
+    BUILDER_IMAGE_REVISION: "4.9.1"
     RUBY_VERSION: "2.7"
   script:
     - bundle exec rake spec
+    - prometheus_rust_mmaped_file=true bundle exec rake spec
+    - cd ext/fast_mmaped_file_rs && cargo nextest run
   artifacts:
     paths:
       - coverage/
 
 ruby:
   <<: *test-job
+  before_script:
+    - apt-get update
+    - apt-get install -y llvm-dev libclang-dev clang
+    - *install-rust
+    - *install-ruby-and-compile
+    - curl -LsSf https://get.nexte.st/0.9/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
   parallel:
     matrix:
-      - RUBY_VERSION: ["2.7", "3.0", "3.1"]
-
+      - RUBY_VERSION: ["2.7", "3.0", "3.1", "3.2"]
 
 builder:centos_7:
   <<: *test-job
+  before_script:
+    - source /opt/rh/llvm-toolset-7/enable
+    - *install-ruby-and-compile
+  script:
+    - *test-ruby
   image: registry.gitlab.com/gitlab-org/gitlab-omnibus-builder/centos_7:${BUILDER_IMAGE_REVISION}
-  cache:
-    key: "centos_7"
-    paths:
-      - vendor/ruby
 
 builder:centos_8:
   <<: *test-job
+  before_script:
+    - *install-ruby-and-compile
+  script:
+    - *test-ruby
   image: registry.gitlab.com/gitlab-org/gitlab-omnibus-builder/centos_8:${BUILDER_IMAGE_REVISION}
-  cache:
-    key: "centos_8"
-    paths:
-      - vendor/ruby
 
 i386/debian:bullseye:
   <<: *test-job
+  image: i386/debian:bullseye
   before_script:
     - apt-get update
-    - apt-get install -y curl ruby ruby-dev build-essential
+    - apt-get install -y curl ruby ruby-dev build-essential llvm-dev libclang-dev clang git
+    - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet --default-toolchain=1.65.0 --profile=minimal --default-host=i686-unknown-linux-gnu
+    - source "$HOME/.cargo/env"
+    - *install-ruby-and-compile
+    - curl -LsSf https://get.nexte.st/0.9/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
     - export LANG=C.UTF-8
-    - gem install bundler rake
-    - bundle install -j $(nproc)
-    - bundle exec rake compile -- --fail-on-warning
   script:
-    - bundle exec rake spec
-  cache:
-    key: "i386-debian-bullseye"
-    paths:
-      - vendor/ruby
+    - *test-ruby
 
 archlinux:ruby:2.5:
   <<: *test-job
   image: archlinux/archlinux:base
   before_script:
-    - pacman -Sy --noconfirm git gcc make ruby ruby-bundler ruby-rdoc ruby-rake which grep gawk procps-ng
-    - bundle install
-    - bundle exec rake compile
+    - pacman -Sy --noconfirm git gcc clang make ruby ruby-bundler ruby-rdoc ruby-rake which grep gawk procps-ng
+    - *install-rust
+    - *install-ruby-and-compile
+    - curl -LsSf https://get.nexte.st/0.9/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin
   script:
-    - bundle exec rake spec
-  cache:
-    key: "archlinux-ruby2.5"
-    paths:
-      - vendor/ruby
+    - *test-ruby
 
 fuzzbert:
   image: ruby:2.7
@@ -69,6 +109,7 @@ fuzzbert:
     prometheus_multiproc_dir: tmp/
   script:
     - bundle exec fuzzbert fuzz/**/fuzz_*.rb --handler PrintAndExitHandler --limit 10000
+    - prometheus_rust_mmaped_file=true bundle exec fuzzbert fuzz/**/fuzz_*.rb --handler PrintAndExitHandler --limit 10000
 
 clang-format check:
   image: debian:bullseye-slim
@@ -79,32 +120,56 @@ clang-format check:
     - find ext/ -name '*.[ch]' | xargs clang-format -style=file -i
     - git diff --exit-code
 
+rustfmt check:
+  image: debian:bullseye-slim
+  before_script:
+    - apt-get update
+    - apt-get install -y curl
+    - *install-rust
+    - rustup component add rustfmt
+  script:
+    - cargo fmt --manifest-path ext/fast_mmaped_file_rs/Cargo.toml -- --check
+
+clippy check:
+  image: debian:bullseye-slim
+  before_script:
+    - apt-get update
+    - apt-get install -y curl ruby ruby-dev build-essential llvm-dev libclang-dev clang git
+    - *install-rust
+    - rustup component add clippy
+  script:
+    - cargo clippy --manifest-path ext/fast_mmaped_file_rs/Cargo.toml
+
 rspec Address-Sanitizer:
   image: debian:bullseye-slim
   before_script:
     - apt-get update
-    - apt-get install -y libasan5 ruby ruby-dev gem build-essential
+    - apt-get install -y git libasan5 ruby ruby-dev gem build-essential curl llvm-dev libclang-dev clang
+    - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet --default-toolchain=nightly-2022-11-03 --profile=minimal
+    - source "$HOME/.cargo/env"
+    - rustup component add rust-src
+    - *install-ruby
   allow_failure: true
   script:
-    - gem install bundler rake
-    - bundle install
-    - rake compile -- --enable-address-sanitizer
+    - RB_SYS_EXTRA_CARGO_ARGS='-Zbuild-std' CARGO_BUILD_TARGET=x86_64-unknown-linux-gnu bundle exec rake compile -- --enable-address-sanitizer
     - export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.5
     - export ASAN_OPTIONS=atexit=true:verbosity=1
+    - prometheus_rust_mmaped_file=true bundle exec rspec > /dev/null
     - bundle exec rspec > /dev/null
 
 parsing Address-Sanitizer:
   image: debian:bullseye-slim
   before_script:
     - apt-get update
-    - apt-get install -y libasan5 ruby ruby-dev gem build-essential
+    - apt-get install -y git libasan5 ruby ruby-dev gem build-essential curl llvm-dev libclang-dev clang
+    - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet --default-toolchain=nightly-2022-11-03 --profile=minimal
+    - source "$HOME/.cargo/env"
+    - rustup component add rust-src
+    - *install-ruby-and-compile
   allow_failure: true
   script:
-    - gem install bundler rake
-    - bundle install
-    - rake compile
     - bundle exec rspec
-    - rake compile -- --enable-address-sanitizer
+    - RB_SYS_EXTRA_CARGO_ARGS='-Zbuild-std' CARGO_BUILD_TARGET=x86_64-unknown-linux-gnu bundle exec rake compile -- --enable-address-sanitizer
     - DB_FILE=$(basename $(find tmp -name '*.db' | head -n 1))
     - test -n $DB_FILE
     - for ((i=1;i<=100;i++)); do cp tmp/$DB_FILE tmp/$i$DB_FILE; done
@@ -113,18 +178,6 @@ parsing Address-Sanitizer:
     - export LSAN_OPTIONS=suppressions=known-leaks-suppression.txt
     - bundle exec bin/parse -t tmp/*.db > /dev/null
 
-cache:
-  paths:
-    - vendor/ruby
-
-before_script:
-  - ruby -v
-  - gem install bundler -v 1.17.3
-  - gem install rake
-  - bundle install -j $(nproc) --path vendor
-  - gcc --version
-  - bundle exec rake compile -- --fail-on-warning
-
 pages:
   image: ruby:2.7
   stage: deploy
@@ -138,3 +191,52 @@ pages:
     expire_in: 30 days
   only:
     - master
+
+gems:
+  image: docker:20.10.16
+  services:
+    - docker:20.10.16-dind
+  stage: build_gems
+  needs: []
+  variables:
+    DOCKER_HOST: tcp://docker:2375
+    DOCKER_TLS_CERTDIR: ""
+  before_script:
+    - apk add ruby ruby-dev git make gcc g++
+    - *install-ruby
+  script:
+    - bundle exec rake gem:${TARGET_PLATFORM}
+  parallel:
+    matrix:
+      - TARGET_PLATFORM: ["aarch64-linux", "arm64-darwin", "x86_64-darwin", "x86_64-linux"]
+  artifacts:
+    paths:
+      - pkg/*.gem
+  only:
+    - tags
+
+gem_smoke_test:
+  stage: build_gems
+  image: ruby:3.1
+  needs: ["gems: [x86_64-linux]"]
+  before_script:
+    - ruby -v
+  script:
+    - gem install pkg/prometheus-client-mmap-*-x86_64-linux.gem
+    - echo "Checking if Rust extension is available..."
+    - ruby -r 'prometheus/client/helper/loader' -e "exit 1 unless Prometheus::Client::Helper::Loader.rust_impl_available?"
+  cache: []
+  only:
+    - tags
+
+release:
+  stage: release
+  image: ruby:3.1
+  before_script:
+    - gem -v
+  script:
+    - ls -al pkg/*.gem
+    - tools/deploy-rubygem.sh
+  cache: []
+  only:
+    - tags
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..a0d130d
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1 @@
+rust 1.65.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1b54370..9b4e1d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,59 @@
+## v0.24.3
+
+- mmap: Use C types when interfacing with Ruby !116
+
+## v0.24.2
+
+- Start tracking shared child strings !114
+- Convert 'type_' to non-static Symbol !115
+
+## v0.24.1
+
+- ci: Fix smoke test !113
+
+## v0.24.0
+
+- Expose Rust extension for use in marshaling metrics and read/write values !111
+- Fix i386/debian CI job and refactor cache key handling !110
+
+## v0.23.1
+
+- Use c_long for Ruby string length !109
+
+## v0.23.0
+
+- Drop musl precompiled gem and relax RubyGems dependency !106
+
+## v0.22.0
+
+- Re-implement write path in Rust !103
+
+## v0.21.0
+
+- Remove 'rustc' check from 'Rakefile'  !97
+- Add support for precompiled gems !99
+- Refactor 'bin/setup' to remove uninitialized vars !100
+- ci: create precompiled gems and push to Rubygems automatically !101
+- Require RubyGems >= v3.3.22 !101
+
+## v0.20.3
+
+- Check for 'rustc' in 'extconf.rb' !95
+
+## v0.20.2
+
+- Allocate EntryMap keys only when needed !92
+- Don't auto-install Rust toolchain on 'gem install' !93
+
+## v0.20.1
+
+- Install Rust extension to 'lib' !90
+
+## v0.20.0
+
+- Use system page size !84
+- Implement 'to_metrics' in Rust !85
+
 ## v0.19.1
 
 - No changes; v0.19.0 gem pulled in some unnecessary files.
diff --git a/Gemfile b/Gemfile
index 44e0a3e..fae0296 100644
--- a/Gemfile
+++ b/Gemfile
@@ -14,6 +14,7 @@ group :test do
   gem 'rack-test'
   gem 'rake'
   gem 'pry'
+  gem 'rb_sys', '~> 0.9'
   gem 'rspec'
   gem 'rubocop', ruby_version?('< 2.0') ? '< 0.42' : nil
   gem 'tins', '< 1.7' if ruby_version?('< 2.0')
diff --git a/README.md b/README.md
index 413963f..63bc642 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,20 @@ http_requests = prometheus.counter(:http_requests, 'A counter of HTTP requests m
 http_requests.increment
 ```
 
+## Rust extension (experimental)
+
+In an effort to improve maintainability, there is now an optional Rust
+implementation that reads the metric files and outputs the multiprocess
+metrics to text. If `rustc` is available, then the Rust extension will
+be built automatically. The `use_rust` keyword argument can be used:
+
+```ruby
+puts Prometheus::Client::Formats::Text.marshal_multiprocess(use_rust: true)
+```
+
+Note that this parameter will likely be deprecated and removed once the Rust
+extension becomes the default mode.
+
 ### Rack middleware
 
 There are two [Rack][2] middlewares available, one to expose a metrics HTTP
@@ -186,6 +200,17 @@ Set `prometheus_multiproc_dir` environment variable to the path where you want m
 prometheus_multiproc_dir=/tmp
 ```
 
+### Multiprocess metrics via Rust extension
+
+If the environment variable `prometheus_rust_multiprocess_metrics=true` is set or if the `rust_multiprocess_metrics`
+configuration setting is `true` and the `fast_mmaped_file_rs` extension is available, it will be used to generate
+multiprocess metrics. This should be significantly faster than the C extension.
+
+### Read and write metrics via Rust extension
+
+If the environment variable `prometheus_rust_mmaped_file=true` is set then if the `fast_mmaped_file_rs`
+extension is available it will be used to read and write metrics from the mmapped file.
+
 ## Pitfalls
 
 ### PID cardinality
@@ -230,6 +255,19 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
 
 To install this gem onto your local machine, run `bundle exec rake install`.
 
+### Releasing a new version
+
+To release a new version:
+
+1. Update `lib/prometheus/client/version.rb` with the version number.
+1. Update `CHANGELOG.md` with the changes in the release.
+1. Create a merge request and merge it to `master`.
+1. Push a new tag to the repository.
+
+The new version with precompiled, native gems will automatically be
+published to [RubyGems](https://rubygems.org/gems/prometheus-client-mmap) when the
+pipeline for the tag completes.
+
 [1]: https://github.com/prometheus/prometheus
 [2]: http://rack.github.io/
 [3]: https://gitlab.com/gitlab-org/prometheus-client-mmap/badges/master/pipeline.svg
diff --git a/Rakefile b/Rakefile
index d04cb03..9e37950 100644
--- a/Rakefile
+++ b/Rakefile
@@ -3,6 +3,15 @@ require 'rspec/core/rake_task'
 require 'rubocop/rake_task'
 require 'rake/extensiontask'
 require 'gem_publisher'
+require 'rb_sys'
+
+cross_rubies = %w[3.2.0 3.1.0 3.0.0 2.7.0]
+cross_platforms = %w[
+  aarch64-linux
+  arm64-darwin
+  x86_64-darwin
+  x86_64-linux
+]
 
 desc 'Default: run specs'
 task default: [:spec]
@@ -33,4 +42,36 @@ end
 gemspec = Gem::Specification.load(File.expand_path('../prometheus-client-mmap.gemspec', __FILE__))
 
 Gem::PackageTask.new(gemspec)
-Rake::ExtensionTask.new('fast_mmaped_file', gemspec)
+
+Rake::ExtensionTask.new('fast_mmaped_file', gemspec) do |ext|
+  ext.cross_compile = true
+  ext.cross_platform = cross_platforms
+end
+
+Rake::ExtensionTask.new('fast_mmaped_file_rs', gemspec) do |ext|
+  ext.source_pattern = "*.{rs,toml}"
+  ext.cross_compile = true
+  ext.cross_platform = cross_platforms
+end
+
+namespace "gem" do
+  task "prepare" do
+    sh "bundle"
+  end
+
+  cross_platforms.each do |plat|
+    desc "Build the native gem for #{plat}"
+    task plat => "prepare" do
+      require "rake_compiler_dock"
+
+      ENV["RCD_IMAGE"] = "rbsys/#{plat}:#{RbSys::VERSION}"
+
+      RakeCompilerDock.sh <<~SH, platform: plat
+        bundle && \
+        RUBY_CC_VERSION="#{cross_rubies.join(":")}" \
+        rake native:#{plat} pkg/#{gemspec.full_name}-#{plat}.gem
+      SH
+    end
+  end
+end
+
diff --git a/bin/parse b/bin/parse
index 04e9207..48f56de 100755
--- a/bin/parse
+++ b/bin/parse
@@ -7,7 +7,7 @@ require 'prometheus/client/helper/metrics_representation'
 require 'json'
 require 'optparse'
 
-require 'fast_mmaped_file'
+require 'fast_mmaped_file_rs'
 
 options = {}
 OptionParser.new do |opts|
@@ -49,7 +49,7 @@ def to_prom_text(files)
   files.map {|f| Prometheus::Client::Helper::PlainFile.new(f) }
     .map { |f| [f.filepath, f.multiprocess_mode.to_sym, f.type.to_sym, f.pid] }
     .transform { |files|
-      FastMmapedFile.to_metrics(files.to_a)
+      FastMmapedFileRs.to_metrics(files.to_a)
     }
 end
 
diff --git a/bin/setup b/bin/setup
index 3519e04..ee1f1cc 100755
--- a/bin/setup
+++ b/bin/setup
@@ -5,3 +5,35 @@ set -vx
 
 bundle install
 bundle exec rake compile
+
+if cargo nextest --version > /dev/null 2>&1; then exit; fi
+
+# Check if rust is managed by 'asdf'
+if command -v cargo | grep '.asdf/shims'; then
+    # This will fail if no rust version has been specified in asdf
+    rust_path="$(asdf where rust)/bin"
+
+# Check for $CARGO_HOME that may not be in $HOME
+# We use '/dev/null' as a fallback value known to be present and not a directory
+elif [ -d "${CARGO_HOME:-/dev/null}/bin" ]; then
+    rust_path="${CARGO_HOME}/bin"
+
+# Default path for rustup.rs
+elif [ -d "${HOME}/.cargo/bin" ]; then
+    rust_path="${HOME}/.cargo/bin"
+else
+    echo "No rust toolchain found, skipping installation of 'cargo nextest'"
+    exit
+fi
+
+if [ "$(uname -s)" = 'Darwin' ]; then
+    host_os='mac'
+elif [ "$(uname -s)" = 'Linux' ] && [ "$(uname -m)" = 'x86_64' ]; then
+    host_os='linux'
+else
+    echo "Auto-install for 'cargo nextest' only available on MacOS and x86_64 Linux. Download manually from https://nexte.st/"
+    exit
+fi
+
+echo "Installing 'cargo nextest'..."
+curl -LsSf "https://get.nexte.st/latest/${host_os}" | tar zxf - -C "${rust_path}"
diff --git a/debian/changelog b/debian/changelog
index b017876..ddbf00d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-prometheus-client-mmap (0.24.3-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 25 May 2023 22:00:19 -0000
+
 ruby-prometheus-client-mmap (0.19.1-1) experimental; urgency=medium
 
   * New upstream version 0.19.1
diff --git a/debian/patches/remove-relative-load.patch b/debian/patches/remove-relative-load.patch
index 0c16a50..997eddb 100644
--- a/debian/patches/remove-relative-load.patch
+++ b/debian/patches/remove-relative-load.patch
@@ -2,9 +2,11 @@ autopkgtest should load from the system path, not relative path
 
 Forwarded: not-needed
 
---- a/spec/prometheus/client/helpers/json_parser_spec.rb
-+++ b/spec/prometheus/client/helpers/json_parser_spec.rb
-@@ -23,7 +23,7 @@
+Index: ruby-prometheus-client-mmap.git/spec/prometheus/client/helpers/json_parser_spec.rb
+===================================================================
+--- ruby-prometheus-client-mmap.git.orig/spec/prometheus/client/helpers/json_parser_spec.rb
++++ ruby-prometheus-client-mmap.git/spec/prometheus/client/helpers/json_parser_spec.rb
+@@ -23,7 +23,7 @@ describe Prometheus::Client::Helper::Jso
      context 'without Oj' do
        before(:all) do
          Object.send(:remove_const, 'Oj')
diff --git a/ext/fast_mmaped_file/extconf.rb b/ext/fast_mmaped_file/extconf.rb
index b1ac0f8..4187a1d 100644
--- a/ext/fast_mmaped_file/extconf.rb
+++ b/ext/fast_mmaped_file/extconf.rb
@@ -4,7 +4,7 @@ require 'fileutils'
 $CFLAGS << ' -std=c99 -D_POSIX_C_SOURCE=200809L -Wall -Wextra'
 
 if enable_config('fail-on-warning')
-  $CFLAGS << ' -Werrno'
+  $CFLAGS << ' -Werror'
 end
 
 if enable_config('debug')
diff --git a/ext/fast_mmaped_file_rs/.cargo/config.toml b/ext/fast_mmaped_file_rs/.cargo/config.toml
new file mode 100644
index 0000000..aad1dbe
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/.cargo/config.toml
@@ -0,0 +1,23 @@
+[target.aarch64-apple-darwin]
+# Without this flag, when linking static libruby, the linker removes symbols
+# (such as `_rb_ext_ractor_safe`) which it thinks are dead code... but they are
+# not, and they need to be included for the `embed` feature to work with static
+# Ruby.
+rustflags = [
+	"-C", "link-dead-code=on",
+	"-C", "link-arg=-undefined",
+	"-C", "link-arg=dynamic_lookup",
+]
+
+[target.x86_64-apple-darwin]
+rustflags = [
+	"-C", "link-dead-code=on",
+	"-C", "link-arg=-undefined",
+	"-C", "link-arg=dynamic_lookup",
+]
+
+[target.aarch64-unknown-linux-gnu]
+rustflags = [ "-C", "link-dead-code=on" ]
+
+[target.x86_64-unknown-linux-gnu]
+rustflags = [ "-C", "link-dead-code=on" ]
diff --git a/ext/fast_mmaped_file_rs/Cargo.lock b/ext/fast_mmaped_file_rs/Cargo.lock
new file mode 100644
index 0000000..4c5b105
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/Cargo.lock
@@ -0,0 +1,790 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bindgen"
+version = "0.60.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bstr"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
+dependencies = [
+ "memchr",
+ "once_cell",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clang-sys"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fast_mmaped_file_rs"
+version = "0.1.0"
+dependencies = [
+ "bstr",
+ "hashbrown",
+ "indoc",
+ "libc",
+ "magnus",
+ "memmap2",
+ "nix",
+ "rand",
+ "rb-sys",
+ "sha2",
+ "smallvec",
+ "tempfile",
+ "thiserror",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "indoc"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f2cb48b81b1dc9f39676bf99f5499babfec7cd8fe14307f7b3d747208fb5690"
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
+[[package]]
+name = "libc"
+version = "0.2.141"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
+
+[[package]]
+name = "magnus"
+version = "0.5.0"
+source = "git+https://github.com/matsadler/magnus?branch=main#b10aab48119eb87a872bf0bb4480b1fcebe5d1b9"
+dependencies = [
+ "magnus-macros",
+ "rb-sys",
+ "rb-sys-env",
+ "seq-macro",
+]
+
+[[package]]
+name = "magnus-macros"
+version = "0.4.0"
+source = "git+https://github.com/matsadler/magnus?branch=main#b10aab48119eb87a872bf0bb4480b1fcebe5d1b9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.13",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memmap2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nix"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "libc",
+ "memoffset",
+ "pin-utils",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rb-sys"
+version = "0.9.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "156bfedced1e236600bcaad538477097ff2ed5c6b474e411d15b791e1d24c0f1"
+dependencies = [
+ "rb-sys-build",
+]
+
+[[package]]
+name = "rb-sys-build"
+version = "0.9.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cb2e4a32cbc290b543a74567072ad24b708aff7bb5dde5a68d5690379cd7938"
+dependencies = [
+ "bindgen",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "shell-words",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "rb-sys-env"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a35802679f07360454b418a5d1735c89716bde01d35b1560fc953c1415a0b3bb"
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.37.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "seq-macro"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6b44e8fc93a14e66336d230954dda83d18b4605ccace8fe09bc7514a71ad0bc"
+
+[[package]]
+name = "serde"
+version = "1.0.159"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
+
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.13",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[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_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[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_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[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_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[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_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
diff --git a/ext/fast_mmaped_file_rs/Cargo.toml b/ext/fast_mmaped_file_rs/Cargo.toml
new file mode 100644
index 0000000..5f5ad0c
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "fast_mmaped_file_rs"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+hashbrown = "0.13"
+libc = "0.2"
+magnus = { git = "https://github.com/matsadler/magnus", branch = "main", features = ["rb-sys"] }
+memmap2 = "0.5"
+# v0.26 cannot be built on CentOS 7 https://github.com/nix-rust/nix/issues/1972
+nix = { version = "0.25", features = ["mman"] } # mman used for MsFlags
+rb-sys = "0.9"
+smallvec = "1.10"
+thiserror = "1.0"
+
+[dev-dependencies]
+bstr = "1.4"
+indoc = "2.0"
+# We need the `embed` feature to run tests, but this triggers failures when building as a Gem.
+magnus = { git = "https://github.com/matsadler/magnus", branch = "main", features = ["rb-sys","embed"] }
+rand = "0.8"
+sha2 = "0.10"
+tempfile = "3.5"
+
+[lib]
+# Integration tests won't work if crate is only `cdylib`.
+crate-type = ["cdylib","lib"]
diff --git a/ext/fast_mmaped_file_rs/README.md b/ext/fast_mmaped_file_rs/README.md
new file mode 100644
index 0000000..a146b7c
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/README.md
@@ -0,0 +1,52 @@
+# Testing
+
+## Running Tests
+
+Use [cargo nextest](https://nexte.st/) to execute the Rust unit tests.
+
+```sh
+$ cargo nextest run
+```
+
+## Why not use 'cargo test'?
+
+We need to embed Ruby into the test binary to access Ruby types. This requires
+us to run `magnus::embed::init()` no more than once before calling Ruby.
+See [the magnus docs](https://docs.rs/magnus/latest/magnus/embed/fn.init.html)
+for more details.
+
+If we try to create separate `#[test]` functions that call `init()` these will
+conflict, as Cargo runs tests in parallel using a single process with separate
+threads. Running `cargo test` will result in errors like:
+
+```
+---- file_info::test::with_ruby stdout ----
+thread 'file_info::test::with_ruby' panicked at 'Ruby already initialized'
+```
+
+The simplest workaround for this is to avoid using `cargo test` to run unit
+tests. [nextest](https://nexte.st/) is an alternate test harness that runs each
+test as its own process, enabling each test to intitialize Ruby without
+conflict.
+
+## 'symbol not found' errors when running tests
+
+If you see errors like the following when running tests:
+
+```
+Caused by:
+  for `fast_mmaped_file_rs`, command `/Users/myuser/prometheus-client-mmap/ext/fast_mmaped_file_rs/target/debug/deps/fast_mmaped_file_rs-c81ccc96a6484e04 --list --format terse` exited with signal 6 (SIGABRT)
+--- stdout:
+
+--- stderr:
+dyld[17861]: symbol not found in flat namespace '_rb_cArray'
+```
+
+Clearing the build cache will resolve the problem.
+
+```sh
+$ cargo clean
+```
+
+This is probably due to separate features being used with `magnus` in
+development builds.
diff --git a/ext/fast_mmaped_file_rs/extconf.rb b/ext/fast_mmaped_file_rs/extconf.rb
new file mode 100644
index 0000000..0787499
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/extconf.rb
@@ -0,0 +1,30 @@
+require "mkmf"
+require "rb_sys/mkmf"
+
+if find_executable('rustc')
+  create_rust_makefile("fast_mmaped_file_rs") do |r|
+    r.auto_install_rust_toolchain = false
+
+    if enable_config('fail-on-warning')
+      r.extra_rustflags = ["-Dwarnings"]
+    end
+
+    if enable_config('debug')
+      r.profile = :dev
+    end
+
+    if enable_config('address-sanitizer')
+      r.extra_rustflags = ["-Zsanitizer=address"]
+    end
+
+    # `rb_sys/mkmf` passes all arguments after `--` directly to `cargo rustc`.
+    # We use this awful hack to keep compatibility with existing flags used by
+    # the C implementation.
+    trimmed_argv = ARGV.take_while { |arg| arg != "--" }
+    ARGV = trimmed_argv
+  end
+else
+  puts 'rustc not found, skipping Rust extension.'
+
+  File.write('Makefile', dummy_makefile($srcdir).join(''))
+end
diff --git a/ext/fast_mmaped_file_rs/src/error.rs b/ext/fast_mmaped_file_rs/src/error.rs
new file mode 100644
index 0000000..3c6185f
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/error.rs
@@ -0,0 +1,174 @@
+use magnus::{exception, Ruby};
+use std::any;
+use std::fmt::Display;
+use std::io;
+use std::path::Path;
+use thiserror::Error;
+
+use crate::util;
+use crate::PROM_EPARSING_ERROR;
+
+/// A lightweight representation of Ruby ExceptionClasses.
+#[derive(PartialEq, Eq, Clone, Copy, Debug)]
+pub enum RubyError {
+    Arg,
+    Encoding,
+    Frozen,
+    Index,
+    Io,
+    NoMem,
+    PromParsing,
+    Runtime,
+    Type,
+}
+
+impl From<RubyError> for magnus::ExceptionClass {
+    fn from(err: RubyError) -> magnus::ExceptionClass {
+        match err {
+            RubyError::Arg => exception::arg_error(),
+            RubyError::Encoding => exception::encoding_error(),
+            RubyError::Frozen => exception::frozen_error(),
+            RubyError::Index => exception::index_error(),
+            RubyError::Io => exception::io_error(),
+            RubyError::NoMem => exception::no_mem_error(),
+            RubyError::Runtime => exception::runtime_error(),
+            RubyError::PromParsing => {
+                // UNWRAP: this will panic if called outside of a Ruby thread.
+                let ruby = Ruby::get().unwrap();
+                ruby.get_inner(&PROM_EPARSING_ERROR)
+            }
+            RubyError::Type => exception::type_error(),
+        }
+    }
+}
+
+/// Errors returned internally within the crate. Methods called directly by Ruby return
+/// `magnus::error::Error` as do functions that interact heavily with Ruby. This can be
+/// converted into a `magnus::error::Error` at the boundary between Rust and Ruby.
+#[derive(PartialEq, Eq, Error, Debug)]
+pub enum MmapError {
+    /// A read or write was made while another thread had mutable access to the mmap.
+    #[error("read/write operation attempted while mmap was being written to")]
+    ConcurrentAccess,
+    /// An error message used to exactly match the messages returned by the C
+    /// implementation.
+    #[error("{0}")]
+    Legacy(String, RubyError),
+    /// A String had invalid UTF-8 sequences.
+    #[error("{0}")]
+    Encoding(String),
+    /// A failed attempt to cast an integer from one type to another.
+    #[error("failed to cast {object_name} {value} from {from} to {to}")]
+    FailedCast {
+        from: &'static str,
+        to: &'static str,
+        value: String,
+        object_name: String,
+    },
+    /// The mmap was frozen when a mutable operation was attempted.
+    #[error("mmap")]
+    Frozen,
+    /// An io operation failed.
+    #[error("failed to {operation} path '{path}': {err}")]
+    Io {
+        operation: String,
+        path: String,
+        err: String,
+    },
+    #[error("string length gt {}", i32::MAX)]
+    KeyLength,
+    /// Failed to allocate memory.
+    #[error("Couldn't allocate for {0} memory")]
+    OutOfMemory(usize),
+    /// A memory operation fell outside of the containers bounds.
+    #[error("offset {index} out of bounds of len {len}")]
+    OutOfBounds { index: String, len: String },
+    /// A numeric operation overflowed.
+    #[error("overflow when {op} {value} and {added} of type {ty}")]
+    Overflow {
+        value: String,
+        added: String,
+        op: String,
+        ty: &'static str,
+    },
+    /// A miscellaneous error.
+    #[error("{0}")]
+    Other(String),
+    /// A failure when parsing a `.db` file containing Prometheus metrics.
+    #[error("{0}")]
+    PromParsing(String),
+    /// No mmap open.
+    #[error("unmapped file")]
+    UnmappedFile,
+    /// A custom error message with `strerror(3)` appended.
+    #[error("{0}")]
+    WithErrno(String),
+}
+
+impl MmapError {
+    pub fn legacy<T: Into<String>>(msg: T, ruby_err: RubyError) -> Self {
+        MmapError::Legacy(msg.into(), ruby_err)
+    }
+
+    pub fn failed_cast<T: Display, U>(value: T, object_name: &str) -> Self {
+        MmapError::FailedCast {
+            from: any::type_name::<T>(),
+            to: any::type_name::<U>(),
+            value: value.to_string(),
+            object_name: object_name.to_string(),
+        }
+    }
+    pub fn io(operation: &str, path: &Path, err: io::Error) -> Self {
+        MmapError::Io {
+            operation: operation.to_string(),
+            path: path.display().to_string(),
+            err: err.to_string(),
+        }
+    }
+
+    pub fn overflowed<T: Display>(value: T, added: T, op: &str) -> Self {
+        MmapError::Overflow {
+            value: value.to_string(),
+            added: added.to_string(),
+            op: op.to_string(),
+            ty: any::type_name::<T>(),
+        }
+    }
+
+    pub fn out_of_bounds<T: Display>(index: T, len: T) -> Self {
+        MmapError::OutOfBounds {
+            index: index.to_string(),
+            len: len.to_string(),
+        }
+    }
+
+    pub fn with_errno<T: Into<String>>(msg: T) -> Self {
+        let strerror = util::strerror(util::errno());
+        MmapError::WithErrno(format!("{}: ({strerror})", msg.into()))
+    }
+
+    pub fn ruby_err(&self) -> RubyError {
+        match self {
+            MmapError::ConcurrentAccess => RubyError::Arg,
+            MmapError::Legacy(_, e) => *e,
+            MmapError::Encoding(_) => RubyError::Encoding,
+            MmapError::Io { .. } => RubyError::Io,
+            MmapError::FailedCast { .. } => RubyError::Arg,
+            MmapError::Frozen => RubyError::Frozen,
+            MmapError::KeyLength => RubyError::Arg,
+            MmapError::Overflow { .. } => RubyError::Arg,
+            MmapError::OutOfBounds { .. } => RubyError::Index,
+            MmapError::OutOfMemory { .. } => RubyError::NoMem,
+            MmapError::Other(_) => RubyError::Arg,
+            MmapError::PromParsing(_) => RubyError::PromParsing,
+            MmapError::UnmappedFile => RubyError::Io,
+            MmapError::WithErrno(_) => RubyError::Io,
+        }
+    }
+}
+
+impl From<MmapError> for magnus::error::Error {
+    fn from(err: MmapError) -> magnus::error::Error {
+        magnus::error::Error::new(err.ruby_err().into(), err.to_string())
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/file_entry.rs b/ext/fast_mmaped_file_rs/src/file_entry.rs
new file mode 100644
index 0000000..5222a43
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/file_entry.rs
@@ -0,0 +1,579 @@
+use magnus::Symbol;
+use std::fmt::Write;
+use std::str;
+
+use crate::error::{MmapError, RubyError};
+use crate::file_info::FileInfo;
+use crate::parser::{self, MetricText};
+use crate::raw_entry::RawEntry;
+use crate::Result;
+use crate::{SYM_GAUGE, SYM_LIVESUM, SYM_MAX, SYM_MIN};
+
+/// A metrics entry extracted from a `*.db` file.
+#[derive(Clone, Debug)]
+pub struct FileEntry {
+    pub data: EntryData,
+    pub meta: EntryMetadata,
+}
+
+/// The primary data payload for a `FileEntry`, the JSON string and the
+/// associated pid, if significant. Used as the key for `EntryMap`.
+#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
+pub struct EntryData {
+    pub json: String,
+    pub pid: Option<String>,
+}
+
+impl<'a> PartialEq<BorrowedData<'a>> for EntryData {
+    fn eq(&self, other: &BorrowedData) -> bool {
+        self.pid.as_deref() == other.pid && self.json == other.json
+    }
+}
+
+impl<'a> TryFrom<BorrowedData<'a>> for EntryData {
+    type Error = MmapError;
+
+    fn try_from(borrowed: BorrowedData) -> Result<Self> {
+        let mut json = String::new();
+        if json.try_reserve_exact(borrowed.json.len()).is_err() {
+            return Err(MmapError::OutOfMemory(borrowed.json.len()));
+        }
+        json.push_str(borrowed.json);
+
+        Ok(Self {
+            json,
+            // Don't bother checking for allocation failure, typically ~10 bytes
+            pid: borrowed.pid.map(|p| p.to_string()),
+        })
+    }
+}
+
+/// A borrowed copy of the JSON string and pid for a `FileEntry`. We use this
+/// to check if a given string/pid combination is present in the `EntryMap`,
+/// copying them to owned values only when needed.
+#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
+pub struct BorrowedData<'a> {
+    pub json: &'a str,
+    pub pid: Option<&'a str>,
+}
+
+impl<'a> BorrowedData<'a> {
+    pub fn new(
+        raw_entry: &'a RawEntry,
+        file_info: &'a FileInfo,
+        pid_significant: bool,
+    ) -> Result<Self> {
+        let json = str::from_utf8(raw_entry.json())
+            .map_err(|e| MmapError::Encoding(format!("invalid UTF-8 in entry JSON: {e}")))?;
+
+        let pid = if pid_significant {
+            Some(file_info.pid.as_str())
+        } else {
+            None
+        };
+
+        Ok(Self { json, pid })
+    }
+}
+
+/// The metadata associated with a `FileEntry`. The value in `EntryMap`.
+#[derive(Clone, Debug)]
+pub struct EntryMetadata {
+    pub multiprocess_mode: Symbol,
+    pub type_: Symbol,
+    pub value: f64,
+}
+
+impl EntryMetadata {
+    /// Construct a new `FileEntry`, copying the JSON string from the `RawEntry`
+    /// into an internal buffer.
+    pub fn new(mmap_entry: &RawEntry, file: &FileInfo) -> Result<Self> {
+        let value = mmap_entry.value();
+
+        Ok(EntryMetadata {
+            multiprocess_mode: file.multiprocess_mode,
+            type_: file.type_,
+            value,
+        })
+    }
+
+    /// Combine values with another `EntryMetadata`.
+    pub fn merge(&mut self, other: &Self) {
+        if self.type_ == SYM_GAUGE {
+            match self.multiprocess_mode {
+                s if s == SYM_MIN => self.value = self.value.min(other.value),
+                s if s == SYM_MAX => self.value = self.value.max(other.value),
+                s if s == SYM_LIVESUM => self.value += other.value,
+                _ => self.value = other.value,
+            }
+        } else {
+            self.value += other.value;
+        }
+    }
+
+    /// Validate if pid is significant for metric.
+    pub fn is_pid_significant(&self) -> bool {
+        let mp = self.multiprocess_mode;
+
+        self.type_ == SYM_GAUGE && !(mp == SYM_MIN || mp == SYM_MAX || mp == SYM_LIVESUM)
+    }
+}
+
+impl FileEntry {
+    /// Convert the sorted entries into a String in Prometheus metrics format.
+    pub fn entries_to_string(entries: Vec<FileEntry>) -> Result<String> {
+        // We guesstimate that lines are ~100 bytes long, preallocate the string to
+        // roughly that size.
+        let mut out = String::new();
+        out.try_reserve(entries.len() * 128)
+            .map_err(|_| MmapError::OutOfMemory(entries.len() * 128))?;
+
+        let mut prev_name: Option<String> = None;
+
+        let entry_count = entries.len();
+        let mut processed_count = 0;
+
+        for entry in entries {
+            let metrics_data = match parser::parse_metrics(&entry.data.json) {
+                Some(m) => m,
+                // We don't exit the function here so the total number of invalid
+                // entries can be calculated below.
+                None => continue,
+            };
+
+            match prev_name.as_ref() {
+                Some(p) if p == metrics_data.family_name => {}
+                _ => {
+                    entry.append_header(metrics_data.family_name, &mut out);
+                    prev_name = Some(metrics_data.family_name.to_owned());
+                }
+            }
+
+            entry.append_entry(metrics_data, &mut out)?;
+
+            writeln!(&mut out, " {}", entry.meta.value)
+                .map_err(|e| MmapError::Other(format!("Failed to append to output: {e}")))?;
+
+            processed_count += 1;
+        }
+
+        if processed_count != entry_count {
+            return Err(MmapError::legacy(
+                format!("Processed entries {processed_count} != map entries {entry_count}"),
+                RubyError::Runtime,
+            ));
+        }
+
+        Ok(out)
+    }
+
+    fn append_header(&self, family_name: &str, out: &mut String) {
+        out.push_str("# HELP ");
+        out.push_str(family_name);
+        out.push_str(" Multiprocess metric\n");
+
+        out.push_str("# TYPE ");
+        out.push_str(family_name);
+        out.push(' ');
+
+        out.push_str(&self.meta.type_.name().expect("name was invalid UTF-8"));
+        out.push('\n');
+    }
+
+    fn append_entry(&self, json_data: MetricText, out: &mut String) -> Result<()> {
+        out.push_str(json_data.metric_name);
+
+        if json_data.labels.is_empty() {
+            if let Some(pid) = self.data.pid.as_ref() {
+                out.push_str("{pid=\"");
+                out.push_str(pid);
+                out.push_str("\"}");
+            }
+
+            return Ok(());
+        }
+
+        out.push('{');
+
+        let it = json_data.labels.iter().zip(json_data.values.iter());
+
+        for (i, (&key, &val)) in it.enumerate() {
+            out.push_str(key);
+
+            out.push_str("=\"");
+
+            // `null` values will be output as `""`.
+            if val != "null" {
+                out.push_str(val);
+            }
+            out.push('"');
+
+            if i < json_data.labels.len() - 1 {
+                out.push(',');
+            }
+        }
+
+        if let Some(pid) = self.data.pid.as_ref() {
+            out.push_str(",pid=\"");
+            out.push_str(pid);
+            out.push('"');
+        }
+
+        out.push('}');
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use bstr::BString;
+    use indoc::indoc;
+
+    use super::*;
+    use crate::file_info::FileInfo;
+    use crate::raw_entry::RawEntry;
+    use crate::testhelper::{TestEntry, TestFile};
+
+    #[test]
+    fn test_entries_to_string() {
+        struct TestCase {
+            name: &'static str,
+            multiprocess_mode: &'static str,
+            json: &'static [&'static str],
+            values: &'static [f64],
+            pids: &'static [&'static str],
+            expected_out: Option<&'static str>,
+            expected_err: Option<MmapError>,
+        }
+
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let tc = vec![
+            TestCase {
+                name: "one metric, pid significant",
+                multiprocess_mode: "all",
+                json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
+                values: &[1.0],
+                pids: &["worker-1"],
+                expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
+                    # TYPE family gauge
+                    name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "one metric, no pid",
+                multiprocess_mode: "min",
+                json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
+                values: &[1.0],
+                pids: &["worker-1"],
+                expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
+                    # TYPE family gauge
+                    name{label_a="value_a",label_b="value_b"} 1
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "floating point shown",
+                multiprocess_mode: "min",
+                json: &[r#"["family","name",["label_a","label_b"],["value_a","value_b"]]"#],
+                values: &[1.5],
+                pids: &["worker-1"],
+                expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
+                    # TYPE family gauge
+                    name{label_a="value_a",label_b="value_b"} 1.5
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "no labels, pid significant",
+                multiprocess_mode: "all",
+                json: &[r#"["family","name",[],[]]"#],
+                values: &[1.0],
+                pids: &["worker-1"],
+                expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
+                    # TYPE family gauge
+                    name{pid="worker-1"} 1
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "no labels, no pid",
+                multiprocess_mode: "min",
+                json: &[r#"["family","name",[],[]]"#],
+                values: &[1.0],
+                pids: &["worker-1"],
+                expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
+                    # TYPE family gauge
+                    name 1
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "two metrics, same family, pid significant",
+                multiprocess_mode: "all",
+                json: &[
+                    r#"["family","first",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["family","second",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0, 2.0],
+                pids: &["worker-1", "worker-1"],
+                expected_out: Some(indoc! {r##"# HELP family Multiprocess metric
+                    # TYPE family gauge
+                    first{label_a="value_a",label_b="value_b",pid="worker-1"} 1
+                    second{label_a="value_a",label_b="value_b",pid="worker-1"} 2
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "two metrics, different family, pid significant",
+                multiprocess_mode: "min",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0, 2.0],
+                pids: &["worker-1", "worker-1"],
+                expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
+                    # TYPE first_family gauge
+                    first_name{label_a="value_a",label_b="value_b"} 1
+                    # HELP second_family Multiprocess metric
+                    # TYPE second_family gauge
+                    second_name{label_a="value_a",label_b="value_b"} 2
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "three metrics, two different families, pid significant",
+                multiprocess_mode: "all",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["first_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["second_family","second_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0, 2.0, 3.0],
+                pids: &["worker-1", "worker-1", "worker-1"],
+                expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
+                    # TYPE first_family gauge
+                    first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
+                    second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 2
+                    # HELP second_family Multiprocess metric
+                    # TYPE second_family gauge
+                    second_name{label_a="value_a",label_b="value_b",pid="worker-1"} 3
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "same metrics, pid significant, separate workers",
+                multiprocess_mode: "all",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0, 2.0],
+                pids: &["worker-1", "worker-2"],
+                expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
+                    # TYPE first_family gauge
+                    first_name{label_a="value_a",label_b="value_b",pid="worker-1"} 1
+                    first_name{label_a="value_a",label_b="value_b",pid="worker-2"} 2
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "same metrics, pid not significant, separate workers",
+                multiprocess_mode: "max",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0, 2.0],
+                pids: &["worker-1", "worker-2"],
+                expected_out: Some(indoc! {r##"# HELP first_family Multiprocess metric
+                    # TYPE first_family gauge
+                    first_name{label_a="value_a",label_b="value_b"} 1
+                    first_name{label_a="value_a",label_b="value_b"} 2
+                    "##}),
+                expected_err: None,
+            },
+            TestCase {
+                name: "entry fails to parse",
+                multiprocess_mode: "min",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"[not valid"#,
+                ],
+                values: &[1.0, 2.0],
+                pids: &["worker-1", "worker-1"],
+                expected_out: None,
+                expected_err: Some(MmapError::legacy(
+                    "Processed entries 1 != map entries 2".to_owned(),
+                    RubyError::Runtime,
+                )),
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+
+            let input_bytes: Vec<BString> = case
+                .json
+                .iter()
+                .zip(case.values)
+                .map(|(&s, &value)| TestEntry::new(s, value).as_bstring())
+                .collect();
+
+            let mut file_infos = Vec::new();
+            for pid in case.pids {
+                let TestFile {
+                    file,
+                    path,
+                    dir: _dir,
+                } = TestFile::new(b"foobar");
+
+                let info = FileInfo {
+                    file,
+                    path,
+                    len: case.json.len(),
+                    multiprocess_mode: Symbol::new(case.multiprocess_mode),
+                    type_: Symbol::new("gauge"),
+                    pid: pid.to_string(),
+                };
+                file_infos.push(info);
+            }
+
+            let file_entries: Vec<FileEntry> = input_bytes
+                .iter()
+                .map(|s| RawEntry::from_slice(s).unwrap())
+                .zip(file_infos)
+                .map(|(entry, info)| {
+                    let meta = EntryMetadata::new(&entry, &info).unwrap();
+                    let borrowed =
+                        BorrowedData::new(&entry, &info, meta.is_pid_significant()).unwrap();
+                    let data = EntryData::try_from(borrowed).unwrap();
+                    FileEntry { data, meta }
+                })
+                .collect();
+
+            let output = FileEntry::entries_to_string(file_entries);
+
+            if let Some(expected_out) = case.expected_out {
+                assert_eq!(
+                    expected_out,
+                    output.as_ref().unwrap(),
+                    "test case: {name} - output"
+                );
+            }
+
+            if let Some(expected_err) = case.expected_err {
+                assert_eq!(
+                    expected_err,
+                    output.unwrap_err(),
+                    "test case: {name} - error"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn test_merge() {
+        struct TestCase {
+            name: &'static str,
+            metric_type: &'static str,
+            multiprocess_mode: &'static str,
+            values: &'static [f64],
+            expected_value: f64,
+        }
+
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let tc = vec![
+            TestCase {
+                name: "gauge max",
+                metric_type: "gauge",
+                multiprocess_mode: "max",
+                values: &[1.0, 5.0],
+                expected_value: 5.0,
+            },
+            TestCase {
+                name: "gauge min",
+                metric_type: "gauge",
+                multiprocess_mode: "min",
+                values: &[1.0, 5.0],
+                expected_value: 1.0,
+            },
+            TestCase {
+                name: "gauge livesum",
+                metric_type: "gauge",
+                multiprocess_mode: "livesum",
+                values: &[1.0, 5.0],
+                expected_value: 6.0,
+            },
+            TestCase {
+                name: "gauge all",
+                metric_type: "gauge",
+                multiprocess_mode: "all",
+                values: &[1.0, 5.0],
+                expected_value: 5.0,
+            },
+            TestCase {
+                name: "not a gauge",
+                metric_type: "histogram",
+                multiprocess_mode: "max",
+                values: &[1.0, 5.0],
+                expected_value: 6.0,
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+            let json = r#"["family","metric",["label_a","label_b"],["value_a","value_b"]]"#;
+
+            let TestFile {
+                file,
+                path,
+                dir: _dir,
+            } = TestFile::new(b"foobar");
+
+            let info = FileInfo {
+                file,
+                path,
+                len: json.len(),
+                multiprocess_mode: Symbol::new(case.multiprocess_mode),
+                type_: Symbol::new(case.metric_type),
+                pid: "worker-1".to_string(),
+            };
+
+            let input_bytes: Vec<BString> = case
+                .values
+                .iter()
+                .map(|&value| TestEntry::new(json, value).as_bstring())
+                .collect();
+
+            let entries: Vec<FileEntry> = input_bytes
+                .iter()
+                .map(|s| RawEntry::from_slice(s).unwrap())
+                .map(|entry| {
+                    let meta = EntryMetadata::new(&entry, &info).unwrap();
+                    let borrowed =
+                        BorrowedData::new(&entry, &info, meta.is_pid_significant()).unwrap();
+                    let data = EntryData::try_from(borrowed).unwrap();
+                    FileEntry { data, meta }
+                })
+                .collect();
+
+            let mut entry_a = entries[0].clone();
+            let entry_b = entries[1].clone();
+            entry_a.meta.merge(&entry_b.meta);
+
+            assert_eq!(
+                case.expected_value, entry_a.meta.value,
+                "test case: {name} - value"
+            );
+        }
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/file_info.rs b/ext/fast_mmaped_file_rs/src/file_info.rs
new file mode 100644
index 0000000..e8a3951
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/file_info.rs
@@ -0,0 +1,190 @@
+use magnus::exception::*;
+use magnus::{Error, RString, Symbol, Value};
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::{self, Read, Seek};
+use std::os::unix::ffi::OsStringExt;
+use std::path::PathBuf;
+
+use crate::err;
+use crate::error::{MmapError, RubyError};
+use crate::util;
+use crate::Result;
+
+/// The details of a `*.db` file.
+#[derive(Debug)]
+pub struct FileInfo {
+    pub file: File,
+    pub path: PathBuf,
+    pub len: usize,
+    pub multiprocess_mode: Symbol,
+    pub type_: Symbol,
+    pub pid: String,
+}
+
+impl FileInfo {
+    /// Receive the details of a file from Ruby and store as a `FileInfo`.
+    pub fn open_from_params(params: &[Value; 4]) -> magnus::error::Result<Self> {
+        if params.len() != 4 {
+            return Err(err!(
+                arg_error(),
+                "wrong number of arguments {} instead of 4",
+                params.len()
+            ));
+        }
+
+        let filepath = RString::from_value(params[0])
+            .ok_or_else(|| err!(arg_error(), "can't convert filepath to String"))?;
+
+        // SAFETY: We immediately copy the string buffer from Ruby, preventing
+        // it from being mutated out from under us.
+        let path_bytes: Vec<_> = unsafe { filepath.as_slice().to_owned() };
+        let path = PathBuf::from(OsString::from_vec(path_bytes));
+
+        let mut file = File::open(&path).map_err(|_| {
+            err!(
+                arg_error(),
+                "Can't open {}, errno: {}",
+                path.display(),
+                util::errno()
+            )
+        })?;
+
+        let stat = file
+            .metadata()
+            .map_err(|_| err!(io_error(), "Can't stat file, errno: {}", util::errno()))?;
+
+        let length = util::cast_chk::<_, usize>(stat.len(), "file size")?;
+
+        let multiprocess_mode = Symbol::from_value(params[1])
+            .ok_or_else(|| err!(arg_error(), "expected multiprocess_mode to be a symbol"))?;
+
+        let type_ = Symbol::from_value(params[2])
+            .ok_or_else(|| err!(arg_error(), "expected file type to be a symbol"))?;
+
+        let pid = RString::from_value(params[3])
+            .ok_or_else(|| err!(arg_error(), "expected pid to be a String"))?;
+
+        file.rewind()
+            .map_err(|_| err!(io_error(), "Can't fseek 0, errno: {}", util::errno()))?;
+
+        Ok(Self {
+            file,
+            path,
+            len: length,
+            multiprocess_mode,
+            type_,
+            pid: pid.to_string()?,
+        })
+    }
+
+    /// Read the contents of the associated file into the buffer provided by
+    /// the caller.
+    pub fn read_from_file(&mut self, buf: &mut Vec<u8>) -> Result<()> {
+        buf.clear();
+        buf.try_reserve(self.len).map_err(|_| {
+            MmapError::legacy(
+                format!("Can't malloc {}, errno: {}", self.len, util::errno()),
+                RubyError::Io,
+            )
+        })?;
+
+        match self.file.read_to_end(buf) {
+            Ok(n) if n == self.len => Ok(()),
+            Ok(_) => Err(MmapError::io(
+                "read",
+                &self.path,
+                io::Error::from(io::ErrorKind::UnexpectedEof),
+            )),
+            Err(e) => Err(MmapError::io("read", &self.path, e)),
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use magnus::{eval, RArray, Symbol};
+    use rand::{thread_rng, Rng};
+    use sha2::{Digest, Sha256};
+
+    use super::*;
+    use crate::testhelper::TestFile;
+
+    #[test]
+    fn test_open_from_params() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let file_data = b"foobar";
+        let TestFile {
+            file: _file,
+            path,
+            dir: _dir,
+        } = TestFile::new(file_data);
+
+        let pid = "worker-1_0";
+        let args = RArray::from_value(
+            eval(&format!("['{}', :max, :gauge, '{pid}']", path.display())).unwrap(),
+        )
+        .unwrap();
+        let arg0 = args.shift().unwrap();
+        let arg1 = args.shift().unwrap();
+        let arg2 = args.shift().unwrap();
+        let arg3 = args.shift().unwrap();
+
+        let out = FileInfo::open_from_params(&[arg0, arg1, arg2, arg3]);
+        assert!(out.is_ok());
+
+        let out = out.unwrap();
+
+        assert_eq!(out.path, path);
+        assert_eq!(out.len, file_data.len());
+        assert_eq!(out.multiprocess_mode, Symbol::new("max"));
+        assert_eq!(out.type_, Symbol::new("gauge"));
+        assert_eq!(out.pid, pid);
+    }
+
+    #[test]
+    fn test_read_from_file() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        const BUF_LEN: usize = 1 << 20; // 1MiB
+
+        // Create a buffer with random data.
+        let mut buf = vec![0u8; BUF_LEN];
+        thread_rng().fill(buf.as_mut_slice());
+
+        let TestFile {
+            file,
+            path,
+            dir: _dir,
+        } = TestFile::new(&buf);
+
+        let mut info = FileInfo {
+            file,
+            path,
+            len: buf.len(),
+            multiprocess_mode: Symbol::new("puma"),
+            type_: Symbol::new("max"),
+            pid: "worker-0_0".to_string(),
+        };
+
+        let mut out_buf = Vec::new();
+        info.read_from_file(&mut out_buf).unwrap();
+
+        assert_eq!(buf.len(), out_buf.len(), "buffer lens");
+
+        let mut in_hasher = Sha256::new();
+        in_hasher.update(&buf);
+        let in_hash = in_hasher.finalize();
+
+        let mut out_hasher = Sha256::new();
+        out_hasher.update(&out_buf);
+        let out_hash = out_hasher.finalize();
+
+        assert_eq!(in_hash, out_hash, "content hashes");
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/lib.rs b/ext/fast_mmaped_file_rs/src/lib.rs
new file mode 100644
index 0000000..6df573b
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/lib.rs
@@ -0,0 +1,79 @@
+use magnus::exception::*;
+use magnus::prelude::*;
+use magnus::value::{Fixnum, Lazy, LazyId};
+use magnus::{class, define_class, exception, function, method, Ruby};
+use std::mem::size_of;
+
+use crate::mmap::MmapedFile;
+
+pub mod error;
+pub mod file_entry;
+pub mod file_info;
+mod macros;
+pub mod map;
+pub mod mmap;
+pub mod parser;
+pub mod raw_entry;
+pub mod util;
+
+#[cfg(test)]
+mod testhelper;
+
+type Result<T> = std::result::Result<T, crate::error::MmapError>;
+
+const MAP_SHARED: i64 = libc::MAP_SHARED as i64;
+const HEADER_SIZE: usize = 2 * size_of::<u32>();
+
+static SYM_GAUGE: LazyId = LazyId::new("gauge");
+static SYM_MIN: LazyId = LazyId::new("min");
+static SYM_MAX: LazyId = LazyId::new("max");
+static SYM_LIVESUM: LazyId = LazyId::new("livesum");
+static SYM_PID: LazyId = LazyId::new("pid");
+static SYM_SAMPLES: LazyId = LazyId::new("samples");
+
+static PROM_EPARSING_ERROR: Lazy<ExceptionClass> = Lazy::new(|_| {
+    let prom_err = define_class(
+        "PrometheusParsingError",
+        exception::runtime_error().as_r_class(),
+    )
+    .expect("failed to create class `PrometheusParsingError`");
+    ExceptionClass::from_value(prom_err.as_value())
+        .expect("failed to create exception class from `PrometheusParsingError`")
+});
+
+#[magnus::init]
+fn init(ruby: &Ruby) -> magnus::error::Result<()> {
+    // Initialize the static symbols
+    LazyId::force(&SYM_GAUGE, ruby);
+    LazyId::force(&SYM_MIN, ruby);
+    LazyId::force(&SYM_MAX, ruby);
+    LazyId::force(&SYM_LIVESUM, ruby);
+    LazyId::force(&SYM_PID, ruby);
+    LazyId::force(&SYM_SAMPLES, ruby);
+
+    // Initialize `PrometheusParsingError` class.
+    Lazy::force(&PROM_EPARSING_ERROR, ruby);
+
+    let klass = define_class("FastMmapedFileRs", class::object())?;
+    klass.undef_default_alloc_func();
+
+    // UNWRAP: We know `MAP_SHARED` fits in a `Fixnum`.
+    klass.const_set("MAP_SHARED", Fixnum::from_i64(MAP_SHARED).unwrap())?;
+
+    klass.define_singleton_method("to_metrics", function!(MmapedFile::to_metrics, 1))?;
+
+    // Required for subclassing to work
+    klass.define_alloc_func::<MmapedFile>();
+    klass.define_singleton_method("new", method!(MmapedFile::new, -1))?;
+    klass.define_method("initialize", method!(MmapedFile::initialize, 1))?;
+    klass.define_method("slice", method!(MmapedFile::slice, -1))?;
+    klass.define_method("sync", method!(MmapedFile::sync, -1))?;
+    klass.define_method("munmap", method!(MmapedFile::munmap, 0))?;
+
+    klass.define_method("used", method!(MmapedFile::load_used, 0))?;
+    klass.define_method("used=", method!(MmapedFile::save_used, 1))?;
+    klass.define_method("fetch_entry", method!(MmapedFile::fetch_entry, 3))?;
+    klass.define_method("upsert_entry", method!(MmapedFile::upsert_entry, 3))?;
+
+    Ok(())
+}
diff --git a/ext/fast_mmaped_file_rs/src/macros.rs b/ext/fast_mmaped_file_rs/src/macros.rs
new file mode 100644
index 0000000..94edf01
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/macros.rs
@@ -0,0 +1,14 @@
+#[macro_export]
+macro_rules! err {
+    (with_errno: $err_t:expr, $($arg:expr),*) => {
+        {
+            let err = format!($($arg),*);
+            let strerror = strerror(errno());
+            Error::new($err_t, format!("{err} ({strerror})"))
+        }
+    };
+
+    ($err_t:expr, $($arg:expr),*) => {
+        Error::new($err_t, format!($($arg),*))
+    };
+}
diff --git a/ext/fast_mmaped_file_rs/src/map.rs b/ext/fast_mmaped_file_rs/src/map.rs
new file mode 100644
index 0000000..5595694
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/map.rs
@@ -0,0 +1,492 @@
+use hashbrown::hash_map::RawEntryMut;
+use hashbrown::HashMap;
+use magnus::{exception::*, Error, RArray};
+use std::hash::{BuildHasher, Hash, Hasher};
+use std::mem::size_of;
+
+use crate::error::MmapError;
+use crate::file_entry::{BorrowedData, EntryData, EntryMetadata, FileEntry};
+use crate::file_info::FileInfo;
+use crate::raw_entry::RawEntry;
+use crate::util::read_u32;
+use crate::Result;
+use crate::{err, HEADER_SIZE};
+
+/// A HashMap of JSON strings and their associated metadata.
+/// Used to print metrics in text format.
+///
+/// The map key is the entry's JSON string and an optional pid string. The latter
+/// allows us to have multiple entries on the map for multiple pids using the
+/// same string.
+#[derive(Default, Debug)]
+pub struct EntryMap(HashMap<EntryData, EntryMetadata>);
+
+impl EntryMap {
+    /// Construct a new EntryMap.
+    pub fn new() -> Self {
+        Self(HashMap::new())
+    }
+
+    /// Given a list of files, read each one into memory and parse the metrics it contains.
+    pub fn aggregate_files(&mut self, list_of_files: RArray) -> magnus::error::Result<()> {
+        // Pre-allocate the `HashMap` and validate we don't OOM. The C implementation
+        // ignores allocation failures here. We perform this check to avoid potential
+        // panics. We assume ~1,000 entries per file, so 72 KiB allocated per file.
+        self.0
+            .try_reserve(list_of_files.len() * 1024)
+            .map_err(|_| {
+                err!(
+                    no_mem_error(),
+                    "Couldn't allocate for {} memory",
+                    size_of::<FileEntry>() * list_of_files.len() * 1024
+                )
+            })?;
+
+        // We expect file sizes between 4KiB and 4MiB. Pre-allocate 16KiB to reduce reallocations
+        // a bit.
+        let mut buf = Vec::new();
+        buf.try_reserve(16_384)
+            .map_err(|_| err!(no_mem_error(), "Couldn't allocate for {} memory", 16_384))?;
+
+        for item in list_of_files.each() {
+            let params = RArray::from_value(item?).expect("file list was not a Ruby Array");
+            if params.len() != 4 {
+                return Err(err!(
+                    arg_error(),
+                    "wrong number of arguments {} instead of 4",
+                    params.len()
+                ));
+            }
+
+            let params = params.to_value_array::<4>()?;
+
+            let mut file_info = FileInfo::open_from_params(&params)?;
+            file_info.read_from_file(&mut buf)?;
+            self.process_buffer(file_info, &buf)?;
+        }
+        Ok(())
+    }
+
+    /// Consume the `EntryMap` and convert the key/value into`FileEntry`
+    /// objects, sorting them by their JSON strings.
+    pub fn into_sorted(self) -> Result<Vec<FileEntry>> {
+        let mut sorted = Vec::new();
+
+        // To match the behavior of the C version, pre-allocate the entries
+        // and check for allocation failure. Generally idiomatic Rust would
+        // `collect` the iterator into a new `Vec` in place, but this panics
+        // if it can't allocate and we want to continue execution in that
+        // scenario.
+        if sorted.try_reserve_exact(self.0.len()).is_err() {
+            return Err(MmapError::OutOfMemory(
+                self.0.len() * size_of::<FileEntry>(),
+            ));
+        }
+
+        sorted.extend(
+            self.0
+                .into_iter()
+                .map(|(data, meta)| FileEntry { data, meta }),
+        );
+
+        sorted.sort_unstable_by(|x, y| x.data.cmp(&y.data));
+
+        Ok(sorted)
+    }
+
+    /// Check if the `EntryMap` already contains the JSON string.
+    /// If yes, update the associated value, if not insert the
+    /// entry into the map.
+    pub fn merge_or_store(&mut self, data: BorrowedData, meta: EntryMetadata) -> Result<()> {
+        // Manually hash the `BorrowedData` and perform an equality check on the
+        // key. This allows us to perform the comparison without allocating a
+        // new `EntryData` that may not be needed.
+        let mut state = self.0.hasher().build_hasher();
+        data.hash(&mut state);
+        let hash = state.finish();
+
+        match self.0.raw_entry_mut().from_hash(hash, |k| k == &data) {
+            RawEntryMut::Vacant(entry) => {
+                // Allocate a new `EntryData` as the JSON/pid combination is
+                // not present in the map.
+                let owned = EntryData::try_from(data)?;
+                entry.insert(owned, meta);
+            }
+            RawEntryMut::Occupied(mut entry) => {
+                let existing = entry.get_mut();
+                existing.merge(&meta);
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Parse metrics data from a `.db` file and store in the `EntryMap`.
+    fn process_buffer(&mut self, file_info: FileInfo, source: &[u8]) -> Result<()> {
+        if source.len() < HEADER_SIZE {
+            // Nothing to read, OK.
+            return Ok(());
+        }
+
+        // CAST: no-op on 32-bit, widening on 64-bit.
+        let used = read_u32(source, 0)? as usize;
+
+        if used > source.len() {
+            return Err(MmapError::PromParsing(format!(
+                "source file {} corrupted, used {used} > file size {}",
+                file_info.path.display(),
+                source.len()
+            )));
+        }
+
+        let mut pos = HEADER_SIZE;
+
+        while pos + size_of::<u32>() < used {
+            let raw_entry = RawEntry::from_slice(&source[pos..used])?;
+
+            if pos + raw_entry.total_len() > used {
+                return Err(MmapError::PromParsing(format!(
+                    "source file {} corrupted, used {used} < stored data length {}",
+                    file_info.path.display(),
+                    pos + raw_entry.total_len()
+                )));
+            }
+
+            let meta = EntryMetadata::new(&raw_entry, &file_info)?;
+            let data = BorrowedData::new(&raw_entry, &file_info, meta.is_pid_significant())?;
+
+            self.merge_or_store(data, meta)?;
+
+            pos += raw_entry.total_len();
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use magnus::Symbol;
+    use std::mem;
+
+    use super::*;
+    use crate::file_entry::FileEntry;
+    use crate::testhelper::{self, TestFile};
+
+    impl EntryData {
+        /// A helper function for tests to convert owned data to references.
+        fn as_borrowed(&self) -> BorrowedData {
+            BorrowedData {
+                json: &self.json,
+                pid: self.pid.as_deref(),
+            }
+        }
+    }
+
+    #[test]
+    fn test_into_sorted() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let entries = vec![
+            FileEntry {
+                data: EntryData {
+                    json: "zzzzzz".to_string(),
+                    pid: Some("worker-0_0".to_string()),
+                },
+                meta: EntryMetadata {
+                    multiprocess_mode: Symbol::new("max"),
+                    type_: Symbol::new("gauge"),
+                    value: 1.0,
+                },
+            },
+            FileEntry {
+                data: EntryData {
+                    json: "zzz".to_string(),
+                    pid: Some("worker-0_0".to_string()),
+                },
+                meta: EntryMetadata {
+                    multiprocess_mode: Symbol::new("max"),
+                    type_: Symbol::new("gauge"),
+                    value: 1.0,
+                },
+            },
+            FileEntry {
+                data: EntryData {
+                    json: "zzzaaa".to_string(),
+                    pid: Some("worker-0_0".to_string()),
+                },
+                meta: EntryMetadata {
+                    multiprocess_mode: Symbol::new("max"),
+                    type_: Symbol::new("gauge"),
+                    value: 1.0,
+                },
+            },
+            FileEntry {
+                data: EntryData {
+                    json: "aaa".to_string(),
+                    pid: Some("worker-0_0".to_string()),
+                },
+                meta: EntryMetadata {
+                    multiprocess_mode: Symbol::new("max"),
+                    type_: Symbol::new("gauge"),
+                    value: 1.0,
+                },
+            },
+            FileEntry {
+                data: EntryData {
+                    json: "ooo".to_string(),
+                    pid: Some("worker-1_0".to_string()),
+                },
+                meta: EntryMetadata {
+                    multiprocess_mode: Symbol::new("all"),
+                    type_: Symbol::new("gauge"),
+                    value: 1.0,
+                },
+            },
+            FileEntry {
+                data: EntryData {
+                    json: "ooo".to_string(),
+                    pid: Some("worker-0_0".to_string()),
+                },
+                meta: EntryMetadata {
+                    multiprocess_mode: Symbol::new("all"),
+                    type_: Symbol::new("gauge"),
+                    value: 1.0,
+                },
+            },
+        ];
+
+        let mut map = EntryMap::new();
+
+        for entry in entries {
+            map.0.insert(entry.data, entry.meta);
+        }
+
+        let result = map.into_sorted();
+        assert!(result.is_ok());
+        let sorted = result.unwrap();
+        assert_eq!(sorted.len(), 6);
+        assert_eq!(sorted[0].data.json, "aaa");
+        assert_eq!(sorted[1].data.json, "ooo");
+        assert_eq!(sorted[1].data.pid.as_deref(), Some("worker-0_0"));
+        assert_eq!(sorted[2].data.json, "ooo");
+        assert_eq!(sorted[2].data.pid.as_deref(), Some("worker-1_0"));
+        assert_eq!(sorted[3].data.json, "zzz");
+        assert_eq!(sorted[4].data.json, "zzzaaa");
+        assert_eq!(sorted[5].data.json, "zzzzzz");
+    }
+
+    #[test]
+    fn test_merge_or_store() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let key = "foobar";
+
+        let starting_entry = FileEntry {
+            data: EntryData {
+                json: key.to_string(),
+                pid: Some("worker-0_0".to_string()),
+            },
+            meta: EntryMetadata {
+                multiprocess_mode: Symbol::new("all"),
+                type_: Symbol::new("gauge"),
+                value: 1.0,
+            },
+        };
+
+        let matching_entry = FileEntry {
+            data: EntryData {
+                json: key.to_string(),
+                pid: Some("worker-0_0".to_string()),
+            },
+            meta: EntryMetadata {
+                multiprocess_mode: Symbol::new("all"),
+                type_: Symbol::new("gauge"),
+                value: 5.0,
+            },
+        };
+
+        let same_key_different_worker = FileEntry {
+            data: EntryData {
+                json: key.to_string(),
+                pid: Some("worker-1_0".to_string()),
+            },
+            meta: EntryMetadata {
+                multiprocess_mode: Symbol::new("all"),
+                type_: Symbol::new("gauge"),
+                value: 100.0,
+            },
+        };
+
+        let unmatched_entry = FileEntry {
+            data: EntryData {
+                json: "another key".to_string(),
+                pid: Some("worker-0_0".to_string()),
+            },
+            meta: EntryMetadata {
+                multiprocess_mode: Symbol::new("all"),
+                type_: Symbol::new("gauge"),
+                value: 1.0,
+            },
+        };
+
+        let mut map = EntryMap::new();
+
+        map.0
+            .insert(starting_entry.data.clone(), starting_entry.meta.clone());
+
+        let matching_borrowed = matching_entry.data.as_borrowed();
+        map.merge_or_store(matching_borrowed, matching_entry.meta)
+            .unwrap();
+
+        assert_eq!(
+            5.0,
+            map.0.get(&starting_entry.data).unwrap().value,
+            "value updated"
+        );
+        assert_eq!(1, map.0.len(), "no entry added");
+
+        let same_key_different_worker_borrowed = same_key_different_worker.data.as_borrowed();
+        map.merge_or_store(
+            same_key_different_worker_borrowed,
+            same_key_different_worker.meta,
+        )
+        .unwrap();
+
+        assert_eq!(
+            5.0,
+            map.0.get(&starting_entry.data).unwrap().value,
+            "value unchanged"
+        );
+
+        assert_eq!(2, map.0.len(), "additional entry added");
+
+        let unmatched_entry_borrowed = unmatched_entry.data.as_borrowed();
+        map.merge_or_store(unmatched_entry_borrowed, unmatched_entry.meta)
+            .unwrap();
+
+        assert_eq!(
+            5.0,
+            map.0.get(&starting_entry.data).unwrap().value,
+            "value unchanged"
+        );
+        assert_eq!(3, map.0.len(), "entry added");
+    }
+
+    #[test]
+    fn test_process_buffer() {
+        struct TestCase {
+            name: &'static str,
+            json: &'static [&'static str],
+            values: &'static [f64],
+            used: Option<u32>,
+            expected_ct: usize,
+            expected_err: Option<MmapError>,
+        }
+
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let tc = vec![
+            TestCase {
+                name: "single entry",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0],
+                used: None,
+                expected_ct: 1,
+                expected_err: None,
+            },
+            TestCase {
+                name: "multiple entries",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    r#"["second_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0, 2.0],
+                used: None,
+                expected_ct: 2,
+                expected_err: None,
+            },
+            TestCase {
+                name: "empty",
+                json: &[],
+                values: &[],
+                used: None,
+                expected_ct: 0,
+                expected_err: None,
+            },
+            TestCase {
+                name: "used too long",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0],
+                used: Some(9999),
+                expected_ct: 0,
+                expected_err: Some(MmapError::PromParsing(String::new())),
+            },
+            TestCase {
+                name: "used too short",
+                json: &[
+                    r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                ],
+                values: &[1.0],
+                used: Some(15),
+                expected_ct: 0,
+                expected_err: Some(MmapError::out_of_bounds(88, 7)),
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+
+            let input_bytes = testhelper::entries_to_db(case.json, case.values, case.used);
+
+            let TestFile {
+                file,
+                path,
+                dir: _dir,
+            } = TestFile::new(&input_bytes);
+
+            let info = FileInfo {
+                file,
+                path,
+                len: case.json.len(),
+                multiprocess_mode: Symbol::new("max"),
+                type_: Symbol::new("gauge"),
+                pid: "worker-1".to_string(),
+            };
+
+            let mut map = EntryMap::new();
+            let result = map.process_buffer(info, &input_bytes);
+
+            assert_eq!(case.expected_ct, map.0.len(), "test case: {name} - count");
+
+            if let Some(expected_err) = case.expected_err {
+                // Validate we have the right enum type for the error. Error
+                // messages contain the temp dir path and can't be predicted
+                // exactly.
+                assert_eq!(
+                    mem::discriminant(&expected_err),
+                    mem::discriminant(&result.unwrap_err()),
+                    "test case: {name} - failure"
+                );
+            } else {
+                assert_eq!(Ok(()), result, "test case: {name} - success");
+
+                assert_eq!(
+                    case.json.len(),
+                    map.0.len(),
+                    "test case: {name} - all entries captured"
+                );
+            }
+        }
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/mmap.rs b/ext/fast_mmaped_file_rs/src/mmap.rs
new file mode 100644
index 0000000..dbfb36b
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/mmap.rs
@@ -0,0 +1,878 @@
+use magnus::exception::*;
+use magnus::prelude::*;
+use magnus::rb_sys::{AsRawValue, FromRawValue};
+use magnus::typed_data::Obj;
+use magnus::value::Fixnum;
+use magnus::{eval, scan_args, Error, Integer, RArray, RClass, RHash, RString, Value};
+use nix::libc::{c_char, c_long, c_ulong};
+use rb_sys::rb_str_new_static;
+use std::fs::File;
+use std::io::{prelude::*, SeekFrom};
+use std::mem;
+use std::path::Path;
+use std::ptr::NonNull;
+use std::sync::RwLock;
+
+use crate::err;
+use crate::error::MmapError;
+use crate::file_entry::FileEntry;
+use crate::map::EntryMap;
+use crate::raw_entry::RawEntry;
+use crate::util::{self, CheckedOps};
+use crate::Result;
+use crate::HEADER_SIZE;
+use inner::InnerMmap;
+
+mod inner;
+
+/// The Ruby `STR_NOEMBED` flag, aka `FL_USER1`.
+const STR_NOEMBED: c_ulong = 1 << (13);
+/// The Ruby `STR_SHARED` flag, aka `FL_USER2`.
+const STR_SHARED: c_ulong = 1 << (14);
+
+/// A Rust struct wrapped in a Ruby object, providing access to a memory-mapped
+/// file used to store, update, and read out Prometheus metrics.
+///
+/// - File format:
+///     - Header:
+///         - 4 bytes: u32 - total size of metrics in file.
+///         - 4 bytes: NUL byte padding.
+///     - Repeating metrics entries:
+///         - 4 bytes: u32 - entry JSON string size.
+///         - `N` bytes: UTF-8 encoded JSON string used as entry key.
+///         - (8 - (4 + `N`) % 8) bytes: 1 to 8 padding space (0x20) bytes to
+///           reach 8-byte alignment.
+///         - 8 bytes: f64 - entry value.
+///
+/// All numbers are saved in native-endian format.
+///
+/// Generated via [luismartingarcia/protocol](https://github.com/luismartingarcia/protocol):
+///
+///
+/// ```
+/// protocol "Used:4,Pad:4,K1 Size:4,K1 Name:4,K1 Value:8,K2 Size:4,K2 Name:4,K2 Value:8"
+///
+/// 0                   1                   2                   3
+/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+/// |  Used |  Pad  |K1 Size|K1 Name|   K1 Value    |K2 Size|K2 Name|
+/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+/// |  K2 Value   |
+/// +-+-+-+-+-+-+-+
+/// ```
+//
+// The API imposed by `magnus` requires all methods to use shared borrows.
+// This means we can't store any mutable state in the top-level struct,
+// and must store the interior data behind a `RwLock`, which adds run-time
+// checks that mutable operations have no concurrent read or writes.
+//
+// We are further limited by the need to support subclassing in Ruby, which
+// requires us to define an allocation function for the class, the
+// `magnus::class::define_alloc_func()` function. This needs a support the
+// `Default` trait, so a `File` cannot directly help by the object being
+// constructed. Having the `RwLock` hold an `Option` of the interior object
+// resolves this.
+#[derive(Debug, Default)]
+#[magnus::wrap(class = "FastMmapedFileRs", free_immediately, size)]
+pub struct MmapedFile(RwLock<Option<InnerMmap>>);
+
+impl MmapedFile {
+    /// call-seq:
+    ///   new(file)
+    ///
+    /// create a new Mmap object
+    ///
+    /// * <em>file</em>
+    ///
+    ///
+    ///     Creates a mapping that's shared with all other processes
+    ///     mapping the same area of the file.
+    pub fn new(klass: RClass, args: &[Value]) -> magnus::error::Result<Obj<Self>> {
+        let args = scan_args::scan_args::<(RString,), (), (), (), (), ()>(args)?;
+        let path = args.required.0;
+
+        let lock = MmapedFile(RwLock::new(None));
+        let obj = Obj::wrap_as(lock, klass);
+
+        let _: Value = obj.funcall("initialize", (path,))?;
+
+        Ok(obj)
+    }
+
+    /// Initialize a new `FastMmapedFileRs` object. This must be defined in
+    /// order for inheritance to work.
+    pub fn initialize(rb_self: Obj<Self>, fname: String) -> magnus::error::Result<()> {
+        let file = File::options()
+            .read(true)
+            .write(true)
+            .open(&fname)
+            .map_err(|_| err!(arg_error(), "Can't open {}", fname))?;
+
+        let inner = InnerMmap::new(fname.into(), file)?;
+        rb_self.insert_inner(inner)?;
+
+        let weak_klass = RClass::from_value(eval("ObjectSpace::WeakMap")?)
+            .ok_or_else(|| err!(no_method_error(), "unable to create WeakMap"))?;
+        let weak_obj_tracker = weak_klass.new_instance(())?;
+
+        // We will need to iterate over strings backed by the mmapped file, but
+        // don't want to prevent the GC from reaping them when the Ruby code
+        // has finished with them. `ObjectSpace::WeakMap` allows us to track
+        // them without extending their lifetime.
+        //
+        // https://ruby-doc.org/core-3.0.0/ObjectSpace/WeakMap.html
+        rb_self.ivar_set("@weak_obj_tracker", weak_obj_tracker)?;
+
+        Ok(())
+    }
+
+    /// Read the list of files provided from Ruby and convert them to a Prometheus
+    /// metrics String.
+    pub fn to_metrics(file_list: RArray) -> magnus::error::Result<String> {
+        let mut map = EntryMap::new();
+        map.aggregate_files(file_list)?;
+
+        let sorted = map.into_sorted()?;
+
+        FileEntry::entries_to_string(sorted).map_err(|e| e.into())
+    }
+
+    /// Document-method: []
+    /// Document-method: slice
+    ///
+    /// call-seq: [](args)
+    ///
+    /// Element reference - with the following syntax:
+    ///
+    ///   self[nth]
+    ///
+    /// retrieve the <em>nth</em> character
+    ///
+    ///   self[start..last]
+    ///
+    /// return a substring from <em>start</em> to <em>last</em>
+    ///
+    ///   self[start, length]
+    ///
+    /// return a substring of <em>lenght</em> characters from <em>start</em>
+    pub fn slice(rb_self: Obj<Self>, args: &[Value]) -> magnus::error::Result<RString> {
+        // The C implementation would trigger a GC cycle via `rb_gc_force_recycle`
+        // if the `MM_PROTECT` flag is set, but in practice this is never used.
+        // We omit this logic, particularly because `rb_gc_force_recycle` is a
+        // no-op as of Ruby 3.1.
+        let rs_self = &*rb_self;
+
+        let str = rs_self.str(rb_self)?;
+        rs_self._slice(rb_self, str, args)
+    }
+
+    fn _slice(
+        &self,
+        rb_self: Obj<Self>,
+        str: RString,
+        args: &[Value],
+    ) -> magnus::error::Result<RString> {
+        let substr: RString = str.funcall("[]", args)?;
+
+        // Track shared child strings which use the same backing storage.
+        if Self::rb_string_is_shared(substr) {
+            (*rb_self).track_rstring(rb_self, substr)?;
+        }
+
+        // The C implementation does this, perhaps to validate that the len we
+        // provided is actually being used.
+        (*rb_self).inner_mut(|inner| {
+            inner.set_len(str.len());
+            Ok(())
+        })?;
+
+        Ok(substr)
+    }
+
+    /// Document-method: msync
+    /// Document-method: sync
+    /// Document-method: flush
+    ///
+    /// call-seq: msync
+    ///
+    /// flush the file
+    pub fn sync(&self, args: &[Value]) -> magnus::error::Result<()> {
+        use nix::sys::mman::MsFlags;
+
+        let mut ms_async = false;
+        let args = scan_args::scan_args::<(), (Option<i32>,), (), (), (), ()>(args)?;
+
+        if let Some(flag) = args.optional.0 {
+            let flag = MsFlags::from_bits(flag).unwrap_or(MsFlags::empty());
+            ms_async = flag.contains(MsFlags::MS_ASYNC);
+        }
+
+        // The `memmap2` crate does not support the `MS_INVALIDATE` flag. We ignore that
+        // flag if passed in, checking only for `MS_ASYNC`. In practice no arguments are ever
+        // passed to this function, but we do this to maintain compatibility with the
+        // C implementation.
+        self.inner_mut(|inner| inner.flush(ms_async))
+            .map_err(|e| e.into())
+    }
+
+    /// Document-method: munmap
+    /// Document-method: unmap
+    ///
+    /// call-seq: munmap
+    ///
+    /// terminate the association
+    pub fn munmap(rb_self: Obj<Self>) -> magnus::error::Result<()> {
+        let rs_self = &*rb_self;
+
+        rs_self.inner_mut(|inner| {
+            // truncate file to actual used size
+            inner.truncate_file()?;
+
+            // We are about to release the backing mmap for Ruby's String
+            // objects. If Ruby attempts to read from them the program will
+            // segfault. We update the length of all Strings to zero so Ruby
+            // does not attempt to access the now invalid address between now
+            // and when GC eventually reaps the objects.
+            //
+            // See the following for more detail:
+            // https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap/-/issues/39
+            // https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap/-/issues/41
+            // https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap/-/merge_requests/80
+            inner.set_len(0);
+            Ok(())
+        })?;
+
+        // Update each String object to be zero-length.
+        let cap = util::cast_chk::<_, c_long>(rs_self.capacity(), "capacity")?;
+        rs_self.update_weak_map(rb_self, rs_self.as_mut_ptr(), cap)?;
+
+        // Remove the `InnerMmap` from the `RwLock`. This will drop
+        // end of this function, unmapping and closing the file.
+        let _ = rs_self.take_inner()?;
+        Ok(())
+    }
+
+    /// Fetch the `used` header from the `.db` file, the length
+    /// in bytes of the data written to the file.
+    pub fn load_used(&self) -> magnus::error::Result<Integer> {
+        let used = self.inner(|inner| inner.load_used())?;
+
+        Ok(Integer::from_u64(used as u64))
+    }
+
+    /// Update the `used` header for the `.db` file, the length
+    /// in bytes of the data written to the file.
+    pub fn save_used(rb_self: Obj<Self>, used: Fixnum) -> magnus::error::Result<Fixnum> {
+        let rs_self = &*rb_self;
+        let used_uint = used.to_u32()?;
+
+        // If the underlying mmap is smaller than the header, then resize to fit.
+        // The file has already been expanded to page size when first opened, so
+        // even if the map is less than HEADER_SIZE, we're not at risk of a
+        // SIGBUS.
+        if rs_self.capacity() < HEADER_SIZE {
+            rs_self.expand_to_fit(rb_self, HEADER_SIZE)?;
+        }
+
+        rs_self.inner_mut(|inner| inner.save_used(used_uint))?;
+
+        Ok(used)
+    }
+
+    /// Fetch the value associated with a key from the mmap.
+    /// If no entry is present, initialize with the default
+    /// value provided.
+    pub fn fetch_entry(
+        rb_self: Obj<Self>,
+        positions: RHash,
+        key: RString,
+        default_value: f64,
+    ) -> magnus::error::Result<f64> {
+        let rs_self = &*rb_self;
+        let position: Option<Fixnum> = positions.lookup(key)?;
+
+        if let Some(pos) = position {
+            let pos = pos.to_usize()?;
+            return rs_self
+                .inner(|inner| inner.load_value(pos))
+                .map_err(|e| e.into());
+        }
+
+        rs_self.check_expand(rb_self, key.len())?;
+
+        let value_offset: usize = rs_self.inner_mut(|inner| {
+            // SAFETY: We must not call any Ruby code for the lifetime of this borrow.
+            unsafe { inner.initialize_entry(key.as_slice(), default_value) }
+        })?;
+
+        // CAST: no-op on 64-bit, widening on 32-bit.
+        positions.aset(key, Integer::from_u64(value_offset as u64))?;
+
+        rs_self.load_value(value_offset)
+    }
+
+    /// Update the value of an existing entry, if present. Otherwise create a new entry
+    /// for the key.
+    pub fn upsert_entry(
+        rb_self: Obj<Self>,
+        positions: RHash,
+        key: RString,
+        value: f64,
+    ) -> magnus::error::Result<f64> {
+        let rs_self = &*rb_self;
+        let position: Option<Fixnum> = positions.lookup(key)?;
+
+        if let Some(pos) = position {
+            let pos = pos.to_usize()?;
+            return rs_self
+                .inner_mut(|inner| {
+                    inner.save_value(pos, value)?;
+
+                    // TODO just return `value` here instead of loading it?
+                    // This is how the C implementation did it, but I don't
+                    // see what the extra load gains us.
+                    inner.load_value(pos)
+                })
+                .map_err(|e| e.into());
+        }
+
+        rs_self.check_expand(rb_self, key.len())?;
+
+        let value_offset: usize = rs_self.inner_mut(|inner| {
+            // SAFETY: We must not call any Ruby code for the lifetime of this borrow.
+            unsafe { inner.initialize_entry(key.as_slice(), value) }
+        })?;
+
+        // CAST: no-op on 64-bit, widening on 32-bit.
+        positions.aset(key, Integer::from_u64(value_offset as u64))?;
+
+        rs_self.load_value(value_offset)
+    }
+
+    /// Creates a Ruby String containing the section of the mmapped file that
+    /// has been written to.
+    fn str(&self, rb_self: Obj<Self>) -> magnus::error::Result<RString> {
+        let val_id = (*rb_self).inner(|inner| {
+            let ptr = inner.as_ptr();
+            let len = inner.len();
+
+            // SAFETY: This is safe so long as the data provided to Ruby meets its
+            // requirements. When unmapping the file this will no longer be the
+            // case, see the comment on `munmap` for how we handle this.
+            Ok(unsafe { rb_str_new_static(ptr as _, len as _) })
+        })?;
+
+        // SAFETY: We know that rb_str_new_static returns a VALUE.
+        let val = unsafe { Value::from_raw(val_id) };
+
+        // UNWRAP: We created this value as a string above.
+        let str = RString::from_value(val).unwrap();
+
+        // Freeze the root string so it can't be mutated out from under any
+        // substrings created. This object is never exposed to callers.
+        str.freeze();
+
+        // Track the RString in our `WeakMap` so we can update its address if
+        // we re-mmap the backing file.
+        (*rb_self).track_rstring(rb_self, str)?;
+
+        Ok(str)
+    }
+
+    /// If we reallocate, any live Ruby strings provided by the `str()` method
+    /// will be invalidated. We need to iterate over them using and update their
+    /// heap pointers to the newly allocated memory region.
+    fn update_weak_map(
+        &self,
+        rb_self: Obj<Self>,
+        old_ptr: *const c_char,
+        old_cap: c_long,
+    ) -> magnus::error::Result<()> {
+        let tracker: Value = rb_self.ivar_get("@weak_obj_tracker")?;
+
+        let new_len = self.inner(|inner| util::cast_chk::<_, c_long>(inner.len(), "mmap len"))?;
+
+        // Iterate over the values of the `WeakMap`.
+        for val in tracker.enumeratorize("each_value", ()) {
+            let rb_string = val?;
+            let str = RString::from_value(rb_string)
+                .ok_or_else(|| err!(arg_error(), "weakmap value was not a string"))?;
+
+            // SAFETY: We're messing with Ruby's internals here, YOLO.
+            unsafe {
+                // Convert the magnus wrapper type to a raw string exposed by `rb_sys`,
+                // which provides access to its internals.
+                let mut raw_str = Self::rb_string_internal(str);
+
+                // Shared string have their own `ptr` and `len` values, but `aux`
+                // is the id of the parent string so the GC can track this
+                // dependency. The `ptr` will always be an offset from the base
+                // address of the mmap, and `len` will be the length of the mmap
+                // less the offset from the base.
+                if Self::rb_string_is_shared(str) && new_len > 0 {
+                    // Calculate how far into the original mmap the shared string
+                    // started and update to the equivalent address in the new
+                    // one.
+                    let substr_ptr = raw_str.as_ref().as_.heap.ptr;
+                    let offset = substr_ptr.offset_from(old_ptr);
+
+                    raw_str.as_mut().as_.heap.ptr = self.as_mut_ptr().offset(offset);
+
+                    let current_len = raw_str.as_ref().as_.heap.len;
+                    let new_shared_len = old_cap + current_len;
+
+                    raw_str.as_mut().as_.heap.len = new_shared_len;
+                    continue;
+                }
+
+                // Update the string to point to the new mmapped file.
+                // We're matching the behavior of Ruby's `str_new_static` function.
+                // See https://github.com/ruby/ruby/blob/e51014f9c05aa65cbf203442d37fef7c12390015/string.c#L1030-L1053
+                //
+                // We deliberately do _NOT_ increment the `capa` field of the
+                // string to match the new `len`. We were initially doing this,
+                // but consistently triggered GCs in the middle of updating the
+                // string pointers, causing a segfault.
+                //
+                // See https://gitlab.com/gitlab-org/ruby/gems/prometheus-client-mmap/-/issues/45
+                raw_str.as_mut().as_.heap.ptr = self.as_mut_ptr();
+                raw_str.as_mut().as_.heap.len = new_len;
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Check that the mmap is large enough to contain the value to be added,
+    /// and expand it to fit if necessary.
+    fn check_expand(&self, rb_self: Obj<Self>, key_len: usize) -> magnus::error::Result<()> {
+        // CAST: no-op on 32-bit, widening on 64-bit.
+        let used = self.inner(|inner| inner.load_used())? as usize;
+        let entry_len = RawEntry::calc_total_len(key_len)?;
+
+        // We need the mmapped region to contain at least one byte beyond the
+        // written data to create a NUL- terminated C string. Validate that
+        // new length does not exactly match or exceed the length of the mmap.
+        while self.capacity() <= used.add_chk(entry_len)? {
+            self.expand_to_fit(rb_self, self.capacity().mul_chk(2)?)?;
+        }
+
+        Ok(())
+    }
+
+    /// Expand the underlying file until it is long enough to fit `target_cap`.
+    /// This will remove the existing mmap, expand the file, then update any
+    /// strings held by the `WeakMap` to point to the newly mmapped address.
+    fn expand_to_fit(&self, rb_self: Obj<Self>, target_cap: usize) -> magnus::error::Result<()> {
+        if target_cap < self.capacity() {
+            return Err(err!(arg_error(), "Can't reduce the size of mmap"));
+        }
+
+        let mut new_cap = self.capacity();
+        while new_cap < target_cap {
+            new_cap = new_cap.mul_chk(2)?;
+        }
+
+        if new_cap != self.capacity() {
+            let old_ptr = self.as_mut_ptr();
+            let old_cap = util::cast_chk::<_, c_long>(self.capacity(), "capacity")?;
+
+            // Drop the old mmap.
+            let (mut file, path) = self.take_inner()?.munmap();
+
+            self.expand_file(&mut file, &path, target_cap)?;
+
+            // Re-mmap the expanded file.
+            let new_inner = InnerMmap::reestablish(path, file, target_cap)?;
+
+            self.insert_inner(new_inner)?;
+
+            return self.update_weak_map(rb_self, old_ptr, old_cap);
+        }
+
+        Ok(())
+    }
+
+    /// Use lseek(2) to seek past the end of the file and write a NUL byte. This
+    /// creates a file hole that expands the size of the file without consuming
+    /// disk space until it is actually written to.
+    fn expand_file(&self, file: &mut File, path: &Path, len: usize) -> Result<()> {
+        if len == 0 {
+            return Err(MmapError::overflowed(0, -1, "adding"));
+        }
+
+        // CAST: no-op on 64-bit, widening on 32-bit.
+        let len = len as u64;
+
+        match file.seek(SeekFrom::Start(len - 1)) {
+            Ok(_) => {}
+            Err(_) => {
+                return Err(MmapError::WithErrno(format!("Can't lseek {}", len - 1)));
+            }
+        }
+
+        match file.write(&[0x0]) {
+            Ok(1) => {}
+            _ => {
+                return Err(MmapError::WithErrno(format!(
+                    "Can't extend {}",
+                    path.display()
+                )));
+            }
+        }
+
+        Ok(())
+    }
+
+    fn track_rstring(&self, rb_self: Obj<Self>, str: RString) -> magnus::error::Result<()> {
+        let tracker: Value = rb_self.ivar_get("@weak_obj_tracker")?;
+
+        // Use the string's Id as the key in the `WeakMap`.
+        let key = str.as_raw();
+        let _: Value = tracker.funcall("[]=", (key, str))?;
+        Ok(())
+    }
+
+    /// The total capacity of the underlying mmap.
+    #[inline]
+    fn capacity(&self) -> usize {
+        // UNWRAP: This is actually infallible, but we need to
+        // wrap it in a `Result` for use with `inner()`.
+        self.inner(|inner| Ok(inner.capacity())).unwrap()
+    }
+
+    fn load_value(&self, position: usize) -> magnus::error::Result<f64> {
+        self.inner(|inner| inner.load_value(position))
+            .map_err(|e| e.into())
+    }
+
+    fn as_mut_ptr(&self) -> *mut c_char {
+        // UNWRAP: This is actually infallible, but we need to
+        // wrap it in a `Result` for use with `inner()`.
+        self.inner(|inner| Ok(inner.as_mut_ptr() as *mut c_char))
+            .unwrap()
+    }
+
+    /// Takes a closure with immutable access to InnerMmap. Will fail if the inner
+    /// object has a mutable borrow or has been dropped.
+    fn inner<F, T>(&self, func: F) -> Result<T>
+    where
+        F: FnOnce(&InnerMmap) -> Result<T>,
+    {
+        let inner_opt = self.0.try_read().map_err(|_| MmapError::ConcurrentAccess)?;
+
+        let inner = inner_opt.as_ref().ok_or(MmapError::UnmappedFile)?;
+
+        func(inner)
+    }
+
+    /// Takes a closure with mutable access to InnerMmap. Will fail if the inner
+    /// object has an existing mutable borrow, or has been dropped.
+    fn inner_mut<F, T>(&self, func: F) -> Result<T>
+    where
+        F: FnOnce(&mut InnerMmap) -> Result<T>,
+    {
+        let mut inner_opt = self
+            .0
+            .try_write()
+            .map_err(|_| MmapError::ConcurrentAccess)?;
+
+        let inner = inner_opt.as_mut().ok_or(MmapError::UnmappedFile)?;
+
+        func(inner)
+    }
+
+    /// Take ownership of the `InnerMmap` from the `RwLock`.
+    /// Will fail if a mutable borrow is already held or the inner
+    /// object has been dropped.
+    fn take_inner(&self) -> Result<InnerMmap> {
+        let mut inner_opt = self
+            .0
+            .try_write()
+            .map_err(|_| MmapError::ConcurrentAccess)?;
+        match (*inner_opt).take() {
+            Some(i) => Ok(i),
+            None => Err(MmapError::UnmappedFile),
+        }
+    }
+
+    /// Move `new_inner` into the `RwLock`.
+    /// Will return an error if a mutable borrow is already held.
+    fn insert_inner(&self, new_inner: InnerMmap) -> Result<()> {
+        let mut inner_opt = self
+            .0
+            .try_write()
+            .map_err(|_| MmapError::ConcurrentAccess)?;
+        (*inner_opt).replace(new_inner);
+
+        Ok(())
+    }
+
+    /// Check if an RString is shared. Shared string use the same underlying
+    /// storage as their parent, taking an offset from the start. By default
+    /// they must run to the end of the parent string.
+    fn rb_string_is_shared(rb_str: RString) -> bool {
+        // SAFETY: We only hold a reference to the raw object for the duration
+        // of this function, and no Ruby code is called.
+        let flags = unsafe {
+            let raw_str = Self::rb_string_internal(rb_str);
+            raw_str.as_ref().basic.flags
+        };
+        let shared_flags = STR_SHARED | STR_NOEMBED;
+
+        flags & shared_flags == shared_flags
+    }
+
+    /// Convert `magnus::RString` into the raw binding used by `rb_sys::RString`.
+    /// We need this to manually change the pointer and length values for strings
+    /// when moving the mmap to a new file.
+    ///
+    /// SAFETY: Calling Ruby code while the returned object is held may result
+    /// in it being mutated or dropped.
+    unsafe fn rb_string_internal(rb_str: RString) -> NonNull<rb_sys::RString> {
+        mem::transmute::<RString, NonNull<rb_sys::RString>>(rb_str)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use magnus::error::Error;
+    use magnus::eval;
+    use magnus::Range;
+    use nix::unistd::{sysconf, SysconfVar};
+    use std::mem::size_of;
+
+    use super::*;
+    use crate::raw_entry::RawEntry;
+    use crate::testhelper::TestFile;
+
+    /// Create a wrapped MmapedFile object.
+    fn create_obj() -> Obj<MmapedFile> {
+        let TestFile {
+            file: _file,
+            path,
+            dir: _dir,
+        } = TestFile::new(&[0u8; 8]);
+
+        let path_str = path.display().to_string();
+        let rpath = RString::new(&path_str);
+
+        eval!("FastMmapedFileRs.new(path)", path = rpath).unwrap()
+    }
+
+    /// Add three entries to the mmap. Expected length is 56, 3x 16-byte
+    /// entries with 8-byte header.
+    fn populate_entries(rb_self: &Obj<MmapedFile>) -> RHash {
+        let positions = RHash::from_value(eval("{}").unwrap()).unwrap();
+
+        MmapedFile::upsert_entry(*rb_self, positions, RString::new("a"), 0.0).unwrap();
+        MmapedFile::upsert_entry(*rb_self, positions, RString::new("b"), 1.0).unwrap();
+        MmapedFile::upsert_entry(*rb_self, positions, RString::new("c"), 2.0).unwrap();
+
+        positions
+    }
+
+    #[test]
+    fn test_new() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let TestFile {
+            file,
+            path,
+            dir: _dir,
+        } = TestFile::new(&[0u8; 8]);
+
+        let path_str = path.display().to_string();
+        let rpath = RString::new(&path_str);
+
+        // Object created successfully
+        let result: std::result::Result<Obj<MmapedFile>, Error> =
+            eval!("FastMmapedFileRs.new(path)", path = rpath);
+        assert!(result.is_ok());
+
+        // Weak map added
+        let obj = result.unwrap();
+        let weak_tracker: Value = obj.ivar_get("@weak_obj_tracker").unwrap();
+        assert_eq!("ObjectSpace::WeakMap", weak_tracker.class().inspect());
+
+        // File expanded to page size
+        let page_size = sysconf(SysconfVar::PAGE_SIZE).unwrap().unwrap() as u64;
+        let stat = file.metadata().unwrap();
+        assert_eq!(page_size, stat.len());
+
+        // Used set to header size
+        assert_eq!(
+            HEADER_SIZE as u64,
+            obj.load_used().unwrap().to_u64().unwrap()
+        );
+    }
+
+    #[test]
+    fn test_slice() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let obj = create_obj();
+        let _ = populate_entries(&obj);
+
+        // Validate header updated with new length
+        let header_range = Range::new(0, HEADER_SIZE, true).unwrap().as_value();
+        let header_slice = MmapedFile::slice(obj, &[header_range]).unwrap();
+        assert_eq!([56, 0, 0, 0, 0, 0, 0, 0], unsafe {
+            header_slice.as_slice()
+        });
+
+        let value_range = Range::new(HEADER_SIZE, 24, true).unwrap().as_value();
+        let value_slice = MmapedFile::slice(obj, &[value_range]).unwrap();
+
+        // Validate string length
+        assert_eq!(1u32.to_ne_bytes(), unsafe { &value_slice.as_slice()[0..4] });
+
+        // Validate string and padding
+        assert_eq!("a   ", unsafe {
+            String::from_utf8_lossy(&value_slice.as_slice()[4..8])
+        });
+
+        // Validate value
+        assert_eq!(0.0f64.to_ne_bytes(), unsafe {
+            &value_slice.as_slice()[8..16]
+        });
+    }
+
+    #[test]
+    fn test_slice_resize() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        fn assert_internals(
+            obj: Obj<MmapedFile>,
+            parent_id: c_ulong,
+            child_id: c_ulong,
+            unshared_id: c_ulong,
+        ) {
+            let rs_self = &*obj;
+            let tracker: Value = obj.ivar_get("@weak_obj_tracker").unwrap();
+
+            let mmap_ptr = rs_self.as_mut_ptr();
+            let mmap_len = rs_self.capacity();
+
+            let mut parent_checked = false;
+            let mut child_checked = false;
+
+            for val in tracker.enumeratorize("each_value", ()) {
+                let rb_string = val.unwrap();
+                let str = RString::from_value(rb_string).unwrap();
+
+                unsafe {
+                    let raw_str = MmapedFile::rb_string_internal(str);
+                    if str.as_raw() == child_id {
+                        assert_eq!(parent_id, raw_str.as_ref().as_.heap.aux.shared);
+
+                        let child_offset =
+                            mmap_len as isize - raw_str.as_ref().as_.heap.len as isize;
+                        assert_eq!(mmap_ptr.offset(child_offset), raw_str.as_ref().as_.heap.ptr);
+
+                        child_checked = true;
+                    } else if str.as_raw() == parent_id {
+                        assert_eq!(parent_id, str.as_raw());
+
+                        assert_eq!(mmap_ptr, raw_str.as_ref().as_.heap.ptr);
+                        assert_eq!(mmap_len as c_long, raw_str.as_ref().as_.heap.len);
+                        assert!(raw_str.as_ref().basic.flags & (STR_SHARED | STR_NOEMBED) > 0);
+                        assert!(str.is_frozen());
+
+                        parent_checked = true;
+                    } else if str.as_raw() == unshared_id {
+                        panic!("tracking unshared string");
+                    } else {
+                        panic!("unknown string");
+                    }
+                }
+            }
+            assert!(parent_checked && child_checked);
+        }
+
+        let obj = create_obj();
+        let _ = populate_entries(&obj);
+
+        let rs_self = &*obj;
+
+        // Create a string containing the full mmap.
+        let parent_str = rs_self.str(obj).unwrap();
+        let parent_id = parent_str.as_raw();
+
+        // Ruby's shared strings are only created when they go to the end of
+        // original string.
+        let len = rs_self.inner(|inner| Ok(inner.len())).unwrap();
+        let shareable_range = Range::new(1, len - 1, false).unwrap().as_value();
+
+        // This string should re-use the parent's buffer with an offset and have
+        // the parent's id in `as.heap.aux.shared`
+        let child_str = rs_self._slice(obj, parent_str, &[shareable_range]).unwrap();
+        let child_id = child_str.as_raw();
+
+        // A range that does not reach the end of the parent will not be shared.
+        assert!(len > 4);
+        let unshareable_range = Range::new(0, 4, false).unwrap().as_value();
+
+        // This string should NOT be tracked, it should own its own buffer.
+        let unshared_str = rs_self
+            ._slice(obj, parent_str, &[unshareable_range])
+            .unwrap();
+        let unshared_id = unshared_str.as_raw();
+        assert!(!MmapedFile::rb_string_is_shared(unshared_str));
+
+        assert_internals(obj, parent_id, child_id, unshared_id);
+
+        let orig_ptr = rs_self.as_mut_ptr();
+        // Expand a bunch to ensure we remap
+        for _ in 0..16 {
+            rs_self.expand_to_fit(obj, rs_self.capacity() * 2).unwrap();
+        }
+        let new_ptr = rs_self.as_mut_ptr();
+        assert!(orig_ptr != new_ptr);
+
+        // If we haven't updated the pointer to the newly remapped file this will segfault.
+        let _: Value = eval!("puts parent", parent = parent_str).unwrap();
+        let _: Value = eval!("puts child", child = child_str).unwrap();
+        let _: Value = eval!("puts unshared", unshared = unshared_str).unwrap();
+
+        // Confirm that tracked strings are still valid.
+        assert_internals(obj, parent_id, child_id, unshared_id);
+    }
+
+    #[test]
+    fn test_dont_fill_mmap() {
+        let _cleanup = unsafe { magnus::embed::init() };
+        let ruby = magnus::Ruby::get().unwrap();
+        crate::init(&ruby).unwrap();
+
+        let obj = create_obj();
+        let positions = populate_entries(&obj);
+
+        let rs_self = &*obj;
+
+        rs_self.expand_to_fit(obj, 1024).unwrap();
+
+        let current_used = rs_self.inner(|inner| inner.load_used()).unwrap() as usize;
+        let current_cap = rs_self.inner(|inner| Ok(inner.len())).unwrap();
+
+        // Create a new entry that exactly fills the capacity of the mmap.
+        let val_len =
+            current_cap - current_used - HEADER_SIZE - size_of::<f64>() - size_of::<u32>();
+        assert_eq!(
+            current_cap,
+            RawEntry::calc_total_len(val_len).unwrap() + current_used
+        );
+
+        let str = String::from_utf8(vec![b'A'; val_len]).unwrap();
+        MmapedFile::upsert_entry(obj, positions, RString::new(&str), 1.0).unwrap();
+
+        // Validate that we have expanded the mmap, ensuring a trailing NUL.
+        assert!(rs_self.capacity() > current_cap);
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/mmap/inner.rs b/ext/fast_mmaped_file_rs/src/mmap/inner.rs
new file mode 100644
index 0000000..ced3ce5
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/mmap/inner.rs
@@ -0,0 +1,714 @@
+use libc::off_t;
+use memmap2::{MmapMut, MmapOptions};
+use nix::libc::c_long;
+use std::fs::File;
+use std::mem::size_of;
+use std::ops::Range;
+use std::os::unix::prelude::{AsRawFd, RawFd};
+use std::path::PathBuf;
+
+use crate::error::{MmapError, RubyError};
+use crate::raw_entry::RawEntry;
+use crate::util::CheckedOps;
+use crate::util::{self, errno, read_f64, read_u32};
+use crate::Result;
+use crate::HEADER_SIZE;
+
+/// A mmapped file and its metadata. Ruby never directly interfaces
+/// with this struct.
+#[derive(Debug)]
+pub(super) struct InnerMmap {
+    /// The handle of the file being mmapped. When resizing the
+    /// file we must drop the `InnerMmap` while keeping this open,
+    /// truncate/extend the file, and establish a new `InnerMmap` to
+    /// re-map it.
+    file: File,
+    /// The path of the file.
+    path: PathBuf,
+    /// The mmap itself. When initializing a new entry the length of
+    /// the mmap is used for bounds checking.
+    map: MmapMut,
+    /// The length of data written to the file, used to validate
+    /// whether a `load/save_value` call is in bounds and the length
+    /// we truncate the file to when unmapping.
+    ///
+    /// Equivalent to `i_mm->t->real` in the C implementation.
+    len: usize,
+}
+
+impl InnerMmap {
+    /// Constructs a new `InnerMmap`, mmapping `path`.
+    /// Use when mmapping a file for the first time. When re-mapping a file
+    /// after expanding it the `reestablish` function should be used.
+    pub fn new(path: PathBuf, file: File) -> Result<Self> {
+        let stat = file.metadata().map_err(|e| {
+            MmapError::legacy(
+                format!("Can't stat {}: {e}", path.display()),
+                RubyError::Arg,
+            )
+        })?;
+
+        let file_size = util::cast_chk::<_, usize>(stat.len(), "file length")?;
+
+        // We need to ensure the underlying file descriptor is at least a page size.
+        // Otherwise, we could get a SIGBUS error if mmap() attempts to read or write
+        // past the file.
+        let reserve_size = Self::next_page_boundary(file_size)?;
+
+        // Cast: no-op.
+        Self::reserve_mmap_file_bytes(file.as_raw_fd(), reserve_size as off_t).map_err(|e| {
+            MmapError::legacy(
+                format!(
+                    "Can't reserve {reserve_size} bytes for memory-mapped file in {}: {e}",
+                    path.display()
+                ),
+                RubyError::Io,
+            )
+        })?;
+
+        // Ensure we always have space for the header.
+        let map_len = file_size.max(HEADER_SIZE);
+
+        // SAFETY: There is the possibility of UB if the file is modified outside of
+        // this program.
+        let map = unsafe { MmapOptions::new().len(map_len).map_mut(&file) }.map_err(|e| {
+            MmapError::legacy(format!("mmap failed ({}): {e}", errno()), RubyError::Arg)
+        })?;
+
+        let len = file_size;
+
+        Ok(Self {
+            file,
+            path,
+            map,
+            len,
+        })
+    }
+
+    /// Re-mmap a file that was previously mapped.
+    pub fn reestablish(path: PathBuf, file: File, map_len: usize) -> Result<Self> {
+        // SAFETY: There is the possibility of UB if the file is modified outside of
+        // this program.
+        let map = unsafe { MmapOptions::new().len(map_len).map_mut(&file) }.map_err(|e| {
+            MmapError::legacy(format!("mmap failed ({}): {e}", errno()), RubyError::Arg)
+        })?;
+
+        // TODO should we keep this as the old len? We'd want to be able to truncate
+        // to the old length at this point if closing the file. Matching C implementation
+        // for now.
+        let len = map_len;
+
+        Ok(Self {
+            file,
+            path,
+            map,
+            len,
+        })
+    }
+
+    /// Add a new metrics entry to the end of the mmap. This will fail if the mmap is at
+    /// capacity. Callers must expand the file first.
+    ///
+    /// SAFETY: Must not call any Ruby code for the lifetime of `key`, otherwise we risk
+    /// Ruby mutating the underlying `RString`.
+    pub unsafe fn initialize_entry(&mut self, key: &[u8], value: f64) -> Result<usize> {
+        // CAST: no-op on 32-bit, widening on 64-bit.
+        let current_used = self.load_used()? as usize;
+        let entry_length = RawEntry::calc_total_len(key.len())?;
+
+        let new_used = current_used.add_chk(entry_length)?;
+
+        // Increasing capacity requires expanding the file and re-mmapping it, we can't
+        // perform this from `InnerMmap`.
+        if self.capacity() < new_used {
+            return Err(MmapError::Other(format!(
+                "mmap capacity {} less than {}",
+                self.capacity(),
+                new_used
+            )));
+        }
+
+        let bytes = self.map.as_mut();
+        let value_offset = RawEntry::save(&mut bytes[current_used..new_used], key, value)?;
+
+        // Won't overflow as value_offset is less than new_used.
+        let position = current_used + value_offset;
+        let new_used32 = util::cast_chk::<_, u32>(new_used, "used")?;
+
+        self.save_used(new_used32)?;
+        Ok(position)
+    }
+
+    /// Save a metrics value to an existing entry in the mmap.
+    pub fn save_value(&mut self, offset: usize, value: f64) -> Result<()> {
+        if self.len.add_chk(size_of::<f64>())? <= offset {
+            return Err(MmapError::out_of_bounds(
+                offset + size_of::<f64>(),
+                self.len,
+            ));
+        }
+
+        if offset < HEADER_SIZE {
+            return Err(MmapError::Other(format!(
+                "writing to offset {offset} would overwrite file header"
+            )));
+        }
+
+        let value_bytes = value.to_ne_bytes();
+        let value_range = self.item_range(offset, value_bytes.len())?;
+
+        let bytes = self.map.as_mut();
+        bytes[value_range].copy_from_slice(&value_bytes);
+
+        Ok(())
+    }
+
+    /// Load a metrics value from an entry in the mmap.
+    pub fn load_value(&self, offset: usize) -> Result<f64> {
+        if self.len.add_chk(size_of::<f64>())? <= offset {
+            return Err(MmapError::out_of_bounds(
+                offset + size_of::<f64>(),
+                self.len,
+            ));
+        }
+        read_f64(self.map.as_ref(), offset)
+    }
+
+    /// The length of data written to the file.
+    /// With a new file this is only set when Ruby calls `slice` on
+    /// `FastMmapedFileRs`, so even if data has been written to the
+    /// mmap attempts to read will fail until a String is created.
+    /// When an existing file is read we set this value immediately.
+    ///
+    /// Equivalent to `i_mm->t->real` in the C implementation.
+    #[inline]
+    pub fn len(&self) -> usize {
+        self.len
+    }
+
+    /// The total length in bytes of the mmapped file.
+    ///
+    /// Equivalent to `i_mm->t->len` in the C implementation.
+    #[inline]
+    pub fn capacity(&self) -> usize {
+        self.map.len()
+    }
+
+    /// Update the length of the mmap considered to be written.
+    pub fn set_len(&mut self, len: usize) {
+        self.len = len;
+    }
+
+    /// Returns a raw pointer to the mmap.
+    pub fn as_ptr(&self) -> *const u8 {
+        self.map.as_ptr()
+    }
+
+    /// Returns a mutable raw pointer to the mmap.
+    /// For use in updating RString internals which requires a mutable pointer.
+    pub fn as_mut_ptr(&self) -> *mut u8 {
+        self.map.as_ptr().cast_mut()
+    }
+
+    /// Perform an msync(2) on the mmap, flushing all changes written
+    /// to disk. The sync may optionally be performed asynchronously.
+    pub fn flush(&mut self, f_async: bool) -> Result<()> {
+        if f_async {
+            self.map
+                .flush_async()
+                .map_err(|_| MmapError::legacy(format!("msync({})", errno()), RubyError::Arg))
+        } else {
+            self.map
+                .flush()
+                .map_err(|_| MmapError::legacy(format!("msync({})", errno()), RubyError::Arg))
+        }
+    }
+
+    /// Truncate the mmapped file to the end of the metrics data.
+    pub fn truncate_file(&mut self) -> Result<()> {
+        // CAST: no-op on 64-bit, widening on 32-bit.
+        let trunc_len = self.len as u64;
+
+        self.file
+            .set_len(trunc_len)
+            .map_err(|e| MmapError::legacy(format!("truncate: {e}"), RubyError::Type))
+    }
+
+    /// Load the `used` header containing the size of the metrics data written.
+    pub fn load_used(&self) -> Result<u32> {
+        match read_u32(self.map.as_ref(), 0) {
+            // CAST: we know HEADER_SIZE fits in a u32.
+            Ok(0) => Ok(HEADER_SIZE as u32),
+            u => u,
+        }
+    }
+
+    /// Update the `used` header to the value provided.
+    /// value provided.
+    pub fn save_used(&mut self, used: u32) -> Result<()> {
+        let bytes = self.map.as_mut();
+        bytes[..size_of::<u32>()].copy_from_slice(&used.to_ne_bytes());
+
+        Ok(())
+    }
+
+    /// Drop self, which performs an munmap(2) on the mmap,
+    /// returning the open `File` and `PathBuf` so the
+    /// caller can expand the file and re-mmap it.
+    pub fn munmap(self) -> (File, PathBuf) {
+        (self.file, self.path)
+    }
+
+    // From https://stackoverflow.com/a/22820221: The difference with
+    // ftruncate(2) is that (on file systems supporting it, e.g. Ext4)
+    // disk space is indeed reserved by posix_fallocate but ftruncate
+    // extends the file by adding holes (and without reserving disk
+    // space).
+    #[cfg(target_os = "linux")]
+    fn reserve_mmap_file_bytes(fd: RawFd, len: off_t) -> nix::Result<()> {
+        nix::fcntl::posix_fallocate(fd, 0, len)
+    }
+
+    // We simplify the reference implementation since we generally
+    // don't need to reserve more than a page size.
+    #[cfg(not(target_os = "linux"))]
+    fn reserve_mmap_file_bytes(fd: RawFd, len: off_t) -> nix::Result<()> {
+        nix::unistd::ftruncate(fd, len)
+    }
+
+    fn item_range(&self, start: usize, len: usize) -> Result<Range<usize>> {
+        let offset_end = start.add_chk(len)?;
+
+        if offset_end >= self.capacity() {
+            return Err(MmapError::out_of_bounds(offset_end, self.capacity()));
+        }
+
+        Ok(start..offset_end)
+    }
+
+    fn next_page_boundary(len: usize) -> Result<c_long> {
+        use nix::unistd::{self, SysconfVar};
+
+        let len = c_long::try_from(len)
+            .map_err(|_| MmapError::failed_cast::<_, c_long>(len, "file len"))?;
+
+        let mut page_size = match unistd::sysconf(SysconfVar::PAGE_SIZE) {
+            Ok(Some(p)) if p > 0 => p,
+            Ok(Some(p)) => {
+                return Err(MmapError::legacy(
+                    format!("Invalid page size {p}"),
+                    RubyError::Io,
+                ))
+            }
+            Ok(None) => {
+                return Err(MmapError::legacy(
+                    "No system page size found",
+                    RubyError::Io,
+                ))
+            }
+            Err(_) => {
+                return Err(MmapError::legacy(
+                    "Failed to get system page size: {e}",
+                    RubyError::Io,
+                ))
+            }
+        };
+
+        while page_size < len {
+            page_size = page_size.mul_chk(2)?;
+        }
+
+        Ok(page_size)
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use nix::unistd::{self, SysconfVar};
+
+    use super::*;
+    use crate::testhelper::{self, TestEntry, TestFile};
+    use crate::HEADER_SIZE;
+
+    #[test]
+    fn test_new() {
+        struct TestCase {
+            name: &'static str,
+            existing: bool,
+            expected_len: usize,
+        }
+
+        let page_size = unistd::sysconf(SysconfVar::PAGE_SIZE).unwrap().unwrap();
+
+        let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
+        let value = 1.0;
+        let entry_len = TestEntry::new(json, value).as_bytes().len();
+
+        let tc = vec![
+            TestCase {
+                name: "empty file",
+                existing: false,
+                expected_len: 0,
+            },
+            TestCase {
+                name: "existing file",
+                existing: true,
+                expected_len: HEADER_SIZE + entry_len,
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+
+            let data = match case.existing {
+                true => testhelper::entries_to_db(&[json], &[1.0], None),
+                false => Vec::new(),
+            };
+
+            let TestFile {
+                file: original_file,
+                path,
+                dir: _dir,
+            } = TestFile::new(&data);
+
+            let original_stat = original_file.metadata().unwrap();
+
+            let inner = InnerMmap::new(path.clone(), original_file).unwrap();
+
+            let updated_file = File::open(&path).unwrap();
+            let updated_stat = updated_file.metadata().unwrap();
+
+            assert!(
+                updated_stat.len() > original_stat.len(),
+                "test case: {name} - file has been extended"
+            );
+
+            assert_eq!(
+                updated_stat.len(),
+                page_size as u64,
+                "test case: {name} - file extended to page size"
+            );
+
+            assert_eq!(
+                inner.capacity() as u64,
+                original_stat.len().max(HEADER_SIZE as u64),
+                "test case: {name} - mmap capacity matches original file len, unless smaller than HEADER_SIZE"
+            );
+
+            assert_eq!(
+                case.expected_len,
+                inner.len(),
+                "test case: {name} - len set"
+            );
+        }
+    }
+
+    #[test]
+    fn test_reestablish() {
+        struct TestCase {
+            name: &'static str,
+            target_len: usize,
+            expected_len: usize,
+        }
+
+        let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
+
+        let tc = vec![TestCase {
+            name: "ok",
+            target_len: 4096,
+            expected_len: 4096,
+        }];
+
+        for case in tc {
+            let name = case.name;
+
+            let data = testhelper::entries_to_db(&[json], &[1.0], None);
+
+            let TestFile {
+                file: original_file,
+                path,
+                dir: _dir,
+            } = TestFile::new(&data);
+
+            let inner =
+                InnerMmap::reestablish(path.clone(), original_file, case.target_len).unwrap();
+
+            assert_eq!(
+                case.target_len,
+                inner.capacity(),
+                "test case: {name} - mmap capacity set to target len",
+            );
+
+            assert_eq!(
+                case.expected_len,
+                inner.len(),
+                "test case: {name} - len set"
+            );
+        }
+    }
+
+    #[test]
+    fn test_initialize_entry() {
+        struct TestCase {
+            name: &'static str,
+            empty: bool,
+            used: Option<u32>,
+            expected_used: Option<u32>,
+            expected_value_offset: Option<usize>,
+            expected_err: Option<MmapError>,
+        }
+
+        let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
+        let value = 1.0;
+        let entry_len = TestEntry::new(json, value).as_bytes().len();
+
+        let tc = vec![
+            TestCase {
+                name: "empty file, not expanded by outer mmap",
+                empty: true,
+                used: None,
+                expected_used: None,
+                expected_value_offset: None,
+                expected_err: Some(MmapError::Other(format!(
+                    "mmap capacity {HEADER_SIZE} less than {}",
+                    entry_len + HEADER_SIZE,
+                ))),
+            },
+            TestCase {
+                name: "data in file",
+                empty: false,
+                used: None,
+                expected_used: Some(HEADER_SIZE as u32 + (entry_len * 2) as u32),
+                expected_value_offset: Some(176),
+                expected_err: None,
+            },
+            TestCase {
+                name: "data in file, invalid used larger than file",
+                empty: false,
+                used: Some(10_000),
+                expected_used: None,
+                expected_value_offset: None,
+                expected_err: Some(MmapError::Other(format!(
+                    "mmap capacity 4096 less than {}",
+                    10_000 + entry_len
+                ))),
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+
+            let data = match case.empty {
+                true => Vec::new(),
+                false => testhelper::entries_to_db(&[json], &[1.0], case.used),
+            };
+
+            let TestFile {
+                file,
+                path,
+                dir: _dir,
+            } = TestFile::new(&data);
+
+            if !case.empty {
+                // Ensure the file is large enough to have additional entries added.
+                // Normally the outer mmap handles this.
+                file.set_len(4096).unwrap();
+            }
+            let mut inner = InnerMmap::new(path, file).unwrap();
+
+            let result = unsafe { inner.initialize_entry(json.as_bytes(), value) };
+
+            if let Some(expected_used) = case.expected_used {
+                assert_eq!(
+                    expected_used,
+                    inner.load_used().unwrap(),
+                    "test case: {name} - used"
+                );
+            }
+
+            if let Some(expected_value_offset) = case.expected_value_offset {
+                assert_eq!(
+                    expected_value_offset,
+                    *result.as_ref().unwrap(),
+                    "test case: {name} - value_offset"
+                );
+            }
+
+            if let Some(expected_err) = case.expected_err {
+                assert_eq!(
+                    expected_err,
+                    result.unwrap_err(),
+                    "test case: {name} - error"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn test_save_value() {
+        let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
+        let value = 1.0;
+        let upper_bound = TestEntry::new(json, value).as_bytes().len() + HEADER_SIZE;
+        let value_offset = upper_bound - size_of::<f64>();
+
+        struct TestCase {
+            name: &'static str,
+            empty: bool,
+            len: Option<usize>,
+            offset: usize,
+            expected_err: Option<MmapError>,
+        }
+
+        let tc = vec![
+            TestCase {
+                name: "existing file, in bounds",
+                empty: false,
+                len: None,
+                offset: upper_bound - size_of::<f64>() - 1,
+                expected_err: None,
+            },
+            TestCase {
+                name: "existing file, out of bounds",
+                empty: false,
+                len: Some(100),
+                offset: upper_bound * 2,
+                expected_err: Some(MmapError::out_of_bounds(
+                    upper_bound * 2 + size_of::<f64>(),
+                    100,
+                )),
+            },
+            TestCase {
+                name: "existing file, off by one",
+                empty: false,
+                len: None,
+                offset: value_offset + 1,
+                expected_err: Some(MmapError::out_of_bounds(
+                    value_offset + 1 + size_of::<f64>(),
+                    upper_bound,
+                )),
+            },
+            TestCase {
+                name: "empty file cannot be saved to",
+                empty: true,
+                len: None,
+                offset: 8,
+                expected_err: Some(MmapError::out_of_bounds(8 + size_of::<f64>(), 0)),
+            },
+            TestCase {
+                name: "overwrite header",
+                empty: false,
+                len: None,
+                offset: 7,
+                expected_err: Some(MmapError::Other(
+                    "writing to offset 7 would overwrite file header".to_string(),
+                )),
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+
+            let mut data = match case.empty {
+                true => Vec::new(),
+                false => testhelper::entries_to_db(&[json], &[1.0], None),
+            };
+
+            if let Some(len) = case.len {
+                // Pad input to desired length.
+                data.append(&mut vec![0xff; len - upper_bound]);
+            }
+
+            let TestFile {
+                file,
+                path,
+                dir: _dir,
+            } = TestFile::new(&data);
+
+            let mut inner = InnerMmap::new(path, file).unwrap();
+
+            let result = inner.save_value(case.offset, value);
+
+            if let Some(expected_err) = case.expected_err {
+                assert_eq!(
+                    expected_err,
+                    result.unwrap_err(),
+                    "test case: {name} - expected err"
+                );
+            } else {
+                assert!(result.is_ok(), "test case: {name} - success");
+
+                assert_eq!(
+                    value,
+                    util::read_f64(&inner.map, case.offset).unwrap(),
+                    "test case: {name} - value saved"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn test_load_value() {
+        let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
+        let value = 1.0;
+        let total_len = TestEntry::new(json, value).as_bytes().len() + HEADER_SIZE;
+        let value_offset = total_len - size_of::<f64>();
+
+        struct TestCase {
+            name: &'static str,
+            offset: usize,
+            expected_err: Option<MmapError>,
+        }
+
+        let tc = vec![
+            TestCase {
+                name: "in bounds",
+                offset: value_offset,
+                expected_err: None,
+            },
+            TestCase {
+                name: "out of bounds",
+                offset: value_offset * 2,
+                expected_err: Some(MmapError::out_of_bounds(
+                    value_offset * 2 + size_of::<f64>(),
+                    total_len,
+                )),
+            },
+            TestCase {
+                name: "off by one",
+                offset: value_offset + 1,
+                expected_err: Some(MmapError::out_of_bounds(
+                    value_offset + 1 + size_of::<f64>(),
+                    total_len,
+                )),
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+
+            let data = testhelper::entries_to_db(&[json], &[1.0], None);
+
+            let TestFile {
+                file,
+                path,
+                dir: _dir,
+            } = TestFile::new(&data);
+
+            let inner = InnerMmap::new(path, file).unwrap();
+
+            let result = inner.load_value(case.offset);
+
+            if let Some(expected_err) = case.expected_err {
+                assert_eq!(
+                    expected_err,
+                    result.unwrap_err(),
+                    "test case: {name} - expected err"
+                );
+            } else {
+                assert!(result.is_ok(), "test case: {name} - success");
+
+                assert_eq!(value, result.unwrap(), "test case: {name} - value loaded");
+            }
+        }
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/parser.rs b/ext/fast_mmaped_file_rs/src/parser.rs
new file mode 100644
index 0000000..7a00454
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/parser.rs
@@ -0,0 +1,346 @@
+use smallvec::SmallVec;
+use std::str;
+
+/// String slices pointing to the fields of a borrowed `Entry`'s JSON data.
+#[derive(PartialEq, Eq, Debug)]
+pub struct MetricText<'a> {
+    pub family_name: &'a str,
+    pub metric_name: &'a str,
+    pub labels: SmallVec<[&'a str; 4]>,
+    pub values: SmallVec<[&'a str; 4]>,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+struct MetricNames<'a> {
+    label_json: &'a str,
+    family_name: &'a str,
+    metric_name: &'a str,
+}
+
+#[derive(PartialEq, Eq, Debug)]
+struct MetricLabelVals<'a> {
+    labels: SmallVec<[&'a str; 4]>,
+    values: SmallVec<[&'a str; 4]>,
+}
+
+/// Parse Prometheus metric data stored in the following format:
+///
+/// ["metric","name",["label_a","label_b"],["value_a","value_b"]]
+///
+/// There will be 1-8 trailing spaces to ensure at least a one-byte
+/// gap between the json and value, and to pad to an 8-byte alignment.
+/// We strip the surrounding double quotes from all items. Values may
+/// or may not have surrounding double quotes, depending on their type.
+pub fn parse_metrics(json: &str) -> Option<MetricText> {
+    // It would be preferable to use `serde_json` here, but the values
+    // may be strings, numbers, or null, and so don't parse easily to a
+    // defined struct. Using `serde_json::Value` is an option, but since
+    // we're just copying the literal values into a buffer this will be
+    // inefficient and verbose.
+
+    // Trim trailing spaces from string before processing. We use
+    // `trim_end_matches()` instead of `trim_end()` because we know the
+    // trailing bytes are always ASCII 0x20 bytes. `trim_end()` will also
+    // check for unicode spaces and consume a few more CPU cycles.
+    let trimmed = json.trim_end_matches(' ');
+
+    let names = parse_names(trimmed)?;
+    let label_json = names.label_json;
+
+    let label_vals = parse_label_values(label_json)?;
+
+    Some(MetricText {
+        family_name: names.family_name,
+        metric_name: names.metric_name,
+        labels: label_vals.labels,
+        values: label_vals.values,
+    })
+}
+
+fn parse_names(json: &str) -> Option<MetricNames> {
+    // Starting with: ["family_name","metric_name",[...
+    if !json.starts_with("[\"") {
+        return None;
+    }
+
+    // Now: family_name","metric_name",[...
+    let remainder = json.get(2..)?;
+
+    let names_end = remainder.find('[')?;
+
+    // Save the rest of the slice to parse for labels later.
+    let label_json = remainder.get(names_end..)?;
+
+    //  Now: family_name","metric_name",
+    let remainder = remainder.get(..names_end)?;
+
+    // Split on commas into:
+    // family_name","metric_name",
+    // ^^^^one^^^^^ ^^^^^two^^^^^
+    let mut token_iter = remainder.split(',');
+
+    // Captured: family_name","metric_name",
+    //           ^^^^^^^^^^^
+    let family_name = token_iter.next()?.trim_end_matches('"');
+
+    // Captured: "family_name","metric_name",
+    //                          ^^^^^^^^^^^
+    let metric_name = token_iter.next()?.trim_matches('"');
+
+    // Confirm the final entry of the iter is empty, the the trailing ','.
+    if !token_iter.next()?.is_empty() {
+        return None;
+    }
+
+    Some(MetricNames {
+        label_json,
+        family_name,
+        metric_name,
+    })
+}
+
+fn parse_label_values(json: &str) -> Option<MetricLabelVals> {
+    // Starting with: ["label_a","label_b"],["value_a", "value_b"]]
+    if !(json.starts_with('[') && json.ends_with("]]")) {
+        return None;
+    }
+
+    // Validate we either have the start of a label string or an
+    // empty array, e.g. `["` or `[]`.
+    if !matches!(json.as_bytes().get(1)?, b'"' | b']') {
+        return None;
+    }
+
+    // Now: "label_a","label_b"
+    let labels_end = json.find(']')?;
+    let label_range = json.get(1..labels_end)?;
+
+    let mut labels = SmallVec::new();
+
+    // Split on commas into:
+    // "label_a","label_b"
+    // ^^^one^^^ ^^^two^^^
+    for label in label_range.split(',') {
+        // Captured: "label_a","label_b"
+        //            ^^^^^^^
+        // If there are no labels, e.g. `[][]`, then don't capture anything.
+        if !label.is_empty() {
+            labels.push(label.trim_matches('"'));
+        }
+    }
+
+    // Now: ],["value_a", "value_b"]]
+    let mut values_range = json.get(labels_end..)?;
+
+    // Validate we have a separating comma with one and only one leading bracket.
+    if !(values_range.starts_with("],[") && values_range.as_bytes().get(3)? != &b'[') {
+        return None;
+    }
+
+    // Now: "value_a", "value_b"]]
+    values_range = values_range.get(3..)?;
+
+    let values_end = values_range.find(']')?;
+
+    // Validate we have only two trailing brackets.
+    if values_range.get(values_end..)?.len() > 2 {
+        return None;
+    }
+
+    // Now: "value_a", "value_b"
+    values_range = values_range.get(..values_end)?;
+
+    let mut values = SmallVec::new();
+
+    // Split on commas into:
+    // "value_a","value_b"
+    // ^^^one^^^ ^^^two^^^
+    for value in values_range.split(',') {
+        // Captured: "value_a","value_b"
+        //           ^^^^^^^^^
+        // If there are no values, e.g. `[][]`, then don't capture anything.
+        if !value.is_empty() {
+            values.push(value.trim_matches('"'));
+        }
+    }
+
+    if values.len() != labels.len() {
+        return None;
+    }
+
+    Some(MetricLabelVals { labels, values })
+}
+
+#[cfg(test)]
+mod test {
+    use smallvec::smallvec;
+
+    use super::*;
+
+    struct TestCase {
+        name: &'static str,
+        input: &'static str,
+        expected: Option<MetricText<'static>>,
+    }
+
+    #[test]
+    fn valid_json() {
+        let tc = vec![
+            TestCase {
+                name: "basic",
+                input: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: Some(MetricText {
+                    family_name: "metric",
+                    metric_name: "name",
+                    labels: smallvec!["label_a", "label_b"],
+                    values: smallvec!["value_a", "value_b"],
+                }),
+            },
+            TestCase {
+                name: "many labels",
+                input: r#"["metric","name",["label_a","label_b","label_c","label_d","label_e"],["value_a","value_b","value_c","value_d","value_e"]]"#,
+
+                expected: Some(MetricText {
+                    family_name: "metric",
+                    metric_name: "name",
+                    labels: smallvec!["label_a", "label_b", "label_c", "label_d", "label_e"],
+                    values: smallvec!["value_a", "value_b", "value_c", "value_d", "value_e"],
+                }),
+            },
+            TestCase {
+                name: "numeric value",
+                input: r#"["metric","name",["label_a","label_b"],["value_a",403]]"#,
+                expected: Some(MetricText {
+                    family_name: "metric",
+                    metric_name: "name",
+                    labels: smallvec!["label_a", "label_b"],
+                    values: smallvec!["value_a", "403"],
+                }),
+            },
+            TestCase {
+                name: "null value",
+                input: r#"["metric","name",["label_a","label_b"],[null,"value_b"]]"#,
+                expected: Some(MetricText {
+                    family_name: "metric",
+                    metric_name: "name",
+                    labels: smallvec!["label_a", "label_b"],
+                    values: smallvec!["null", "value_b"],
+                }),
+            },
+            TestCase {
+                name: "no labels",
+                input: r#"["metric","name",[],[]]"#,
+                expected: Some(MetricText {
+                    family_name: "metric",
+                    metric_name: "name",
+                    labels: smallvec![],
+                    values: smallvec![],
+                }),
+            },
+        ];
+
+        for case in tc {
+            assert_eq!(
+                parse_metrics(case.input),
+                case.expected,
+                "test case: {}",
+                case.name,
+            );
+        }
+    }
+
+    #[test]
+    fn invalid_json() {
+        let tc = vec![
+            TestCase {
+                name: "not json",
+                input: "hello, world",
+                expected: None,
+            },
+            TestCase {
+                name: "no names",
+                input: r#"[["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many names",
+                input: r#"["metric","name","unexpected_name",["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many labels",
+                input: r#"["metric","name","unexpected_name",["label_a","label_b","label_c"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many values",
+                input: r#"["metric","name",["label_a","label_b"],["value_a","value_b",null]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "no values",
+                input: r#"["metric","name",["label_a","label_b"]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "no arrays",
+                input: r#"["metric","name","label_a","value_a"]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many leading brackets",
+                input: r#"[["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many trailing brackets",
+                input: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many leading label brackets",
+                input: r#"["metric","name",[["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many trailing label brackets",
+                input: r#"["metric","name",["label_a","label_b"]],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "too many leading value brackets",
+                input: r#"["metric","name",["label_a","label_b"],[["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "comma in family name",
+                input: r#"["met,ric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "comma in metric name",
+                input: r#"["metric","na,me",["label_a","label_b"],["value_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "comma in value",
+                input: r#"["metric","na,me",["label_a","label_b"],["val,ue_a","value_b"]]"#,
+                expected: None,
+            },
+            TestCase {
+                name: "comma in numeric value",
+                input: r#"["metric","name",["label_a","label_b"],[400,0,"value_b"]]"#,
+                expected: None,
+            },
+        ];
+
+        for case in tc {
+            assert_eq!(
+                case.expected,
+                parse_metrics(case.input),
+                "test case: {}",
+                case.name,
+            );
+        }
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/raw_entry.rs b/ext/fast_mmaped_file_rs/src/raw_entry.rs
new file mode 100644
index 0000000..337cded
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/raw_entry.rs
@@ -0,0 +1,473 @@
+use std::mem::size_of;
+
+use crate::error::MmapError;
+use crate::util;
+use crate::util::CheckedOps;
+use crate::Result;
+
+/// The logic to save a `MetricsEntry`, or parse one from a byte slice.
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub struct RawEntry<'a> {
+    bytes: &'a [u8],
+    encoded_len: usize,
+}
+
+impl<'a> RawEntry<'a> {
+    /// Save an entry to the mmap, returning the value offset in the newly created entry.
+    pub fn save(bytes: &'a mut [u8], key: &[u8], value: f64) -> Result<usize> {
+        let total_len = Self::calc_total_len(key.len())?;
+
+        if total_len > bytes.len() {
+            return Err(MmapError::Other(format!(
+                "entry length {total_len} larger than slice length {}",
+                bytes.len()
+            )));
+        }
+
+        // CAST: `calc_len` runs `check_encoded_len`, we know the key len
+        // is less than i32::MAX. No risk of overflows or failed casts.
+        let key_len: u32 = key.len() as u32;
+
+        // Write the key length to the mmap.
+        bytes[..size_of::<u32>()].copy_from_slice(&key_len.to_ne_bytes());
+
+        // Advance slice past the size.
+        let bytes = &mut bytes[size_of::<u32>()..];
+
+        bytes[..key.len()].copy_from_slice(key);
+
+        // Advance to end of key.
+        let bytes = &mut bytes[key.len()..];
+
+        let pad_len = Self::padding_len(key.len());
+        bytes[..pad_len].fill(b' ');
+        let bytes = &mut bytes[pad_len..];
+
+        bytes[..size_of::<f64>()].copy_from_slice(&value.to_ne_bytes());
+
+        Self::calc_value_offset(key.len())
+    }
+
+    /// Parse a byte slice starting into an `MmapEntry`.
+    pub fn from_slice(bytes: &'a [u8]) -> Result<Self> {
+        // CAST: no-op on 32-bit, widening on 64-bit.
+        let encoded_len = util::read_u32(bytes, 0)? as usize;
+
+        let total_len = Self::calc_total_len(encoded_len)?;
+
+        // Confirm the value is in bounds of the slice provided.
+        if total_len > bytes.len() {
+            return Err(MmapError::out_of_bounds(total_len, bytes.len()));
+        }
+
+        // Advance slice past length int and cut at end of entry.
+        let bytes = &bytes[size_of::<u32>()..total_len];
+
+        Ok(Self { bytes, encoded_len })
+    }
+
+    /// Read the `f64` value of an entry from memory.
+    #[inline]
+    pub fn value(&self) -> f64 {
+        // We've stripped off the leading u32, don't include that here.
+        let offset = self.encoded_len + Self::padding_len(self.encoded_len);
+
+        // UNWRAP: We confirm in the constructor that the value offset
+        // is in-range for the slice.
+        util::read_f64(self.bytes, offset).unwrap()
+    }
+
+    /// The length of the entry key without padding.
+    #[inline]
+    pub fn encoded_len(&self) -> usize {
+        self.encoded_len
+    }
+
+    /// Returns a slice with the JSON string in the entry, excluding padding.
+    #[inline]
+    pub fn json(&self) -> &[u8] {
+        &self.bytes[..self.encoded_len]
+    }
+
+    /// Calculate the total length of an `MmapEntry`, including the string length,
+    /// string, padding, and value.
+    #[inline]
+    pub fn total_len(&self) -> usize {
+        // UNWRAP:: We confirmed in the constructor that this doesn't overflow.
+        Self::calc_total_len(self.encoded_len).unwrap()
+    }
+
+    /// Calculate the total length of an `MmapEntry`, including the string length,
+    /// string, padding, and value. Validates encoding_len is within expected bounds.
+    #[inline]
+    pub fn calc_total_len(encoded_len: usize) -> Result<usize> {
+        Self::calc_value_offset(encoded_len)?.add_chk(size_of::<f64>())
+    }
+
+    /// Calculate the value offset of an `MmapEntry`, including the string length,
+    /// string, padding. Validates encoding_len is within expected bounds.
+    #[inline]
+    pub fn calc_value_offset(encoded_len: usize) -> Result<usize> {
+        Self::check_encoded_len(encoded_len)?;
+
+        Ok(size_of::<u32>() + encoded_len + Self::padding_len(encoded_len))
+    }
+
+    /// Calculate the number of padding bytes to add to the value key to reach
+    /// 8-byte alignment. Does not validate key length.
+    #[inline]
+    pub fn padding_len(encoded_len: usize) -> usize {
+        8 - (size_of::<u32>() + encoded_len) % 8
+    }
+
+    #[inline]
+    fn check_encoded_len(encoded_len: usize) -> Result<()> {
+        if encoded_len as u64 > i32::MAX as u64 {
+            return Err(MmapError::KeyLength);
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use bstr::ByteSlice;
+
+    use super::*;
+    use crate::testhelper::TestEntry;
+
+    #[test]
+    fn test_from_slice() {
+        #[derive(PartialEq, Default, Debug)]
+        struct TestCase {
+            name: &'static str,
+            input: TestEntry,
+            expected_enc_len: Option<usize>,
+            expected_err: Option<MmapError>,
+        }
+
+        let tc = vec![
+            TestCase {
+                name: "ok",
+                input: TestEntry {
+                    header: 61,
+                    json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    padding_len: 7,
+                    value: 1.0,
+                },
+                expected_enc_len: Some(61),
+                ..Default::default()
+            },
+            TestCase {
+                name: "zero length key",
+                input: TestEntry {
+                    header: 0,
+                    json: "",
+                    padding_len: 4,
+                    value: 1.0,
+                },
+                expected_enc_len: Some(0),
+                ..Default::default()
+            },
+            TestCase {
+                name: "header value too large",
+                input: TestEntry {
+                    header: i32::MAX as u32 + 1,
+                    json: "foo",
+                    padding_len: 1,
+                    value: 0.0,
+                },
+                expected_err: Some(MmapError::KeyLength),
+                ..Default::default()
+            },
+            TestCase {
+                name: "header value much longer than json len",
+                input: TestEntry {
+                    header: 256,
+                    json: "foobar",
+                    padding_len: 6,
+                    value: 1.0,
+                },
+                expected_err: Some(MmapError::out_of_bounds(272, 24)),
+                ..Default::default()
+            },
+            TestCase {
+                // Situations where encoded_len is wrong but padding makes the
+                // value offset the same are not caught.
+                name: "header off by one",
+                input: TestEntry {
+                    header: 4,
+                    json: "123",
+                    padding_len: 1,
+                    value: 1.0,
+                },
+                expected_err: Some(MmapError::out_of_bounds(24, 16)),
+                ..Default::default()
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+            let input = case.input.as_bstring();
+
+            let resp = RawEntry::from_slice(&input);
+
+            if case.expected_err.is_none() {
+                let expected_buf = case.input.as_bytes_no_header();
+                let resp = resp.as_ref().unwrap();
+                let bytes = resp.bytes;
+
+                assert_eq!(expected_buf, bytes.as_bstr(), "test case: {name} - bytes",);
+
+                assert_eq!(
+                    resp.json(),
+                    case.input.json.as_bytes(),
+                    "test case: {name} - json matches"
+                );
+
+                assert_eq!(
+                    resp.total_len(),
+                    case.input.as_bstring().len(),
+                    "test case: {name} - total_len matches"
+                );
+
+                assert_eq!(
+                    resp.encoded_len(),
+                    case.input.json.len(),
+                    "test case: {name} - encoded_len matches"
+                );
+
+                assert!(
+                    resp.json().iter().all(|&c| c != b' '),
+                    "test case: {name} - no spaces in json"
+                );
+
+                let padding_len = RawEntry::padding_len(case.input.json.len());
+                assert!(
+                    bytes[resp.encoded_len..resp.encoded_len + padding_len]
+                        .iter()
+                        .all(|&c| c == b' '),
+                    "test case: {name} - padding is spaces"
+                );
+
+                assert_eq!(
+                    resp.value(),
+                    case.input.value,
+                    "test case: {name} - value is correct"
+                );
+            }
+
+            if let Some(expected_enc_len) = case.expected_enc_len {
+                assert_eq!(
+                    expected_enc_len,
+                    resp.as_ref().unwrap().encoded_len,
+                    "test case: {name} - encoded len",
+                );
+            }
+
+            if let Some(expected_err) = case.expected_err {
+                assert_eq!(expected_err, resp.unwrap_err(), "test case: {name} - error",);
+            }
+        }
+    }
+
+    #[test]
+    fn test_save() {
+        struct TestCase {
+            name: &'static str,
+            key: &'static [u8],
+            value: f64,
+            buf_len: usize,
+            expected_entry: Option<TestEntry>,
+            expected_resp: Result<usize>,
+        }
+
+        // TODO No test case to validate keys with len > i32::MAX, adding a static that large crashes
+        // the test binary.
+        let tc = vec![
+            TestCase {
+                name: "ok",
+                key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                value: 256.0,
+                buf_len: 256,
+                expected_entry: Some(TestEntry {
+                    header: 61,
+                    json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    padding_len: 7,
+                    value: 256.0,
+                }),
+                expected_resp: Ok(72),
+            },
+            TestCase {
+                name: "zero length key",
+                key: b"",
+                value: 1.0,
+                buf_len: 256,
+                expected_entry: Some(TestEntry {
+                    header: 0,
+                    json: "",
+                    padding_len: 4,
+                    value: 1.0,
+                }),
+                expected_resp: Ok(8),
+            },
+            TestCase {
+                name: "infinite value",
+                key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                value: f64::INFINITY,
+                buf_len: 256,
+                expected_entry: Some(TestEntry {
+                    header: 61,
+                    json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    padding_len: 7,
+                    value: f64::INFINITY,
+                }),
+                expected_resp: Ok(72),
+            },
+            TestCase {
+                name: "buf len matches entry len",
+                key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                value: 1.0,
+                buf_len: 80,
+                expected_entry: Some(TestEntry {
+                    header: 61,
+                    json: r#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                    padding_len: 7,
+                    value: 1.0,
+                }),
+                expected_resp: Ok(72),
+            },
+            TestCase {
+                name: "buf much too short",
+                key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                value: 1.0,
+                buf_len: 5,
+                expected_entry: None,
+                expected_resp: Err(MmapError::Other(format!(
+                    "entry length {} larger than slice length {}",
+                    80, 5,
+                ))),
+            },
+            TestCase {
+                name: "buf short by one",
+                key: br#"["metric","name",["label_a","label_b"],["value_a","value_b"]]"#,
+                value: 1.0,
+                buf_len: 79,
+                expected_entry: None,
+                expected_resp: Err(MmapError::Other(format!(
+                    "entry length {} larger than slice length {}",
+                    80, 79,
+                ))),
+            },
+        ];
+
+        for case in tc {
+            let mut buf = vec![0; case.buf_len];
+            let resp = RawEntry::save(&mut buf, case.key, case.value);
+
+            assert_eq!(
+                case.expected_resp, resp,
+                "test case: {} - response",
+                case.name,
+            );
+
+            if let Some(e) = case.expected_entry {
+                let expected_buf = e.as_bstring();
+
+                assert_eq!(
+                    expected_buf,
+                    buf[..expected_buf.len()].as_bstr(),
+                    "test case: {} - buffer state",
+                    case.name
+                );
+
+                let header_len = u32::from_ne_bytes(buf[..size_of::<u32>()].try_into().unwrap());
+                assert_eq!(
+                    case.key.len(),
+                    header_len as usize,
+                    "test case: {} - size header",
+                    case.name,
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn test_calc_value_offset() {
+        struct TestCase {
+            name: &'static str,
+            encoded_len: usize,
+            expected_value_offset: Option<usize>,
+            expected_total_len: Option<usize>,
+            expected_err: Option<MmapError>,
+        }
+
+        let tc = vec![
+            TestCase {
+                name: "ok",
+                encoded_len: 8,
+                expected_value_offset: Some(16),
+                expected_total_len: Some(24),
+                expected_err: None,
+            },
+            TestCase {
+                name: "padding length one",
+                encoded_len: 3,
+                expected_value_offset: Some(8),
+                expected_total_len: Some(16),
+                expected_err: None,
+            },
+            TestCase {
+                name: "padding length eight",
+                encoded_len: 4,
+                expected_value_offset: Some(16),
+                expected_total_len: Some(24),
+                expected_err: None,
+            },
+            TestCase {
+                name: "encoded len gt i32::MAX",
+                encoded_len: i32::MAX as usize + 1,
+                expected_value_offset: None,
+                expected_total_len: None,
+                expected_err: Some(MmapError::KeyLength),
+            },
+        ];
+
+        for case in tc {
+            let name = case.name;
+            if let Some(expected_value_offset) = case.expected_value_offset {
+                assert_eq!(
+                    expected_value_offset,
+                    RawEntry::calc_value_offset(case.encoded_len).unwrap(),
+                    "test case: {name} - value offset"
+                );
+            }
+
+            if let Some(expected_total_len) = case.expected_total_len {
+                assert_eq!(
+                    expected_total_len,
+                    RawEntry::calc_total_len(case.encoded_len).unwrap(),
+                    "test case: {name} - total len"
+                );
+            }
+
+            if let Some(expected_err) = case.expected_err {
+                assert_eq!(
+                    expected_err,
+                    RawEntry::calc_value_offset(case.encoded_len).unwrap_err(),
+                    "test case: {name} - err"
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn test_padding_len() {
+        for encoded_len in 0..64 {
+            let padding = RawEntry::padding_len(encoded_len);
+
+            // Validate we're actually aligning to 8 bytes.
+            assert!((size_of::<u32>() + encoded_len + padding) % 8 == 0)
+        }
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/testhelper.rs b/ext/fast_mmaped_file_rs/src/testhelper.rs
new file mode 100644
index 0000000..4ba9398
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/testhelper.rs
@@ -0,0 +1,222 @@
+use bstr::{BString, B};
+use std::fs::File;
+use std::io::{Read, Seek, Write};
+use std::path::PathBuf;
+use tempfile::{tempdir, TempDir};
+
+use crate::raw_entry::RawEntry;
+use crate::HEADER_SIZE;
+
+#[derive(PartialEq, Default, Debug)]
+pub struct TestEntry {
+    pub header: u32,
+    pub json: &'static str,
+    pub padding_len: usize,
+    pub value: f64,
+}
+
+impl TestEntry {
+    pub fn new(json: &'static str, value: f64) -> Self {
+        TestEntry {
+            header: json.len() as u32,
+            json,
+            padding_len: RawEntry::padding_len(json.len()),
+            value,
+        }
+    }
+
+    pub fn as_bytes(&self) -> Vec<u8> {
+        [
+            B(&self.header.to_ne_bytes()),
+            self.json.as_bytes(),
+            &vec![b' '; self.padding_len],
+            B(&self.value.to_ne_bytes()),
+        ]
+        .concat()
+    }
+    pub fn as_bstring(&self) -> BString {
+        [
+            B(&self.header.to_ne_bytes()),
+            self.json.as_bytes(),
+            &vec![b' '; self.padding_len],
+            B(&self.value.to_ne_bytes()),
+        ]
+        .concat()
+        .into()
+    }
+
+    pub fn as_bytes_no_header(&self) -> BString {
+        [
+            self.json.as_bytes(),
+            &vec![b' '; self.padding_len],
+            B(&self.value.to_ne_bytes()),
+        ]
+        .concat()
+        .into()
+    }
+}
+
+/// Format the data for a `.db` file.
+/// Optional header value can be used to set an invalid `used` size.
+pub fn entries_to_db(entries: &[&'static str], values: &[f64], header: Option<u32>) -> Vec<u8> {
+    let mut out = Vec::new();
+
+    let entry_bytes: Vec<_> = entries
+        .iter()
+        .zip(values)
+        .flat_map(|(e, val)| TestEntry::new(e, *val).as_bytes())
+        .collect();
+
+    let used = match header {
+        Some(u) => u,
+        None => (entry_bytes.len() + HEADER_SIZE) as u32,
+    };
+
+    out.extend(used.to_ne_bytes());
+    out.extend([0x0u8; 4]); // Padding.
+    out.extend(entry_bytes);
+
+    out
+}
+
+/// A temporary file, path, and dir for use with testing.
+#[derive(Debug)]
+pub struct TestFile {
+    pub file: File,
+    pub path: PathBuf,
+    pub dir: TempDir,
+}
+
+impl TestFile {
+    pub fn new(file_data: &[u8]) -> TestFile {
+        let dir = tempdir().unwrap();
+        let path = dir.path().join("test.db");
+        let mut file = File::options()
+            .create(true)
+            .read(true)
+            .write(true)
+            .open(&path)
+            .unwrap();
+
+        file.write_all(file_data).unwrap();
+        file.sync_all().unwrap();
+        file.rewind().unwrap();
+
+        // We need to keep `dir` in scope so it doesn't drop before the files it
+        // contains, which may prevent cleanup.
+        TestFile { file, path, dir }
+    }
+}
+
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_entry_new() {
+        let json = "foobar";
+        let value = 1.0f64;
+        let expected = TestEntry {
+            header: 6,
+            json,
+            padding_len: 6,
+            value,
+        };
+
+        let actual = TestEntry::new(json, value);
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_entry_bytes() {
+        let json = "foobar";
+        let value = 1.0f64;
+        let expected = [
+            &6u32.to_ne_bytes(),
+            B(json),
+            &[b' '; 6],
+            &value.to_ne_bytes(),
+        ]
+        .concat();
+
+        let actual = TestEntry::new(json, value).as_bstring();
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_entry_bytes_no_header() {
+        let json = "foobar";
+        let value = 1.0f64;
+        let expected = [B(json), &[b' '; 6], &value.to_ne_bytes()].concat();
+
+        let actual = TestEntry::new(json, value).as_bytes_no_header();
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn test_entries_to_db_header_correct() {
+        let json = &["foobar", "qux"];
+        let values = &[1.0, 2.0];
+
+        let out = entries_to_db(json, values, None);
+
+        assert_eq!(48u32.to_ne_bytes(), out[0..4], "used set correctly");
+        assert_eq!([0u8; 4], out[4..8], "padding set");
+        assert_eq!(
+            TestEntry::new(json[0], values[0]).as_bytes(),
+            out[8..32],
+            "first entry matches"
+        );
+        assert_eq!(
+            TestEntry::new(json[1], values[1]).as_bytes(),
+            out[32..48],
+            "second entry matches"
+        );
+    }
+
+    #[test]
+    fn test_entries_to_db_header_wrong() {
+        let json = &["foobar", "qux"];
+        let values = &[1.0, 2.0];
+
+        const WRONG_USED: u32 = 1000;
+        let out = entries_to_db(json, values, Some(WRONG_USED));
+
+        assert_eq!(
+            WRONG_USED.to_ne_bytes(),
+            out[0..4],
+            "used set to value requested"
+        );
+        assert_eq!([0u8; 4], out[4..8], "padding set");
+        assert_eq!(
+            TestEntry::new(json[0], values[0]).as_bytes(),
+            out[8..32],
+            "first entry matches"
+        );
+        assert_eq!(
+            TestEntry::new(json[1], values[1]).as_bytes(),
+            out[32..48],
+            "second entry matches"
+        );
+    }
+
+    #[test]
+    fn test_file() {
+        let mut test_file = TestFile::new(b"foobar");
+        let stat = test_file.file.metadata().unwrap();
+
+        assert_eq!(6, stat.len(), "file length");
+        assert_eq!(
+            0,
+            test_file.file.stream_position().unwrap(),
+            "at start of file"
+        );
+        let mut out_buf = vec![0u8; 256];
+        let read_result = test_file.file.read(&mut out_buf);
+        assert!(read_result.is_ok());
+        assert_eq!(6, read_result.unwrap(), "file is readable");
+
+        let write_result = test_file.file.write(b"qux");
+        assert!(write_result.is_ok());
+        assert_eq!(3, write_result.unwrap(), "file is writable");
+    }
+}
diff --git a/ext/fast_mmaped_file_rs/src/util.rs b/ext/fast_mmaped_file_rs/src/util.rs
new file mode 100644
index 0000000..b61e5be
--- /dev/null
+++ b/ext/fast_mmaped_file_rs/src/util.rs
@@ -0,0 +1,121 @@
+use nix::errno::Errno;
+use nix::libc::c_long;
+use std::fmt::Display;
+use std::io;
+use std::mem::size_of;
+
+use crate::error::MmapError;
+use crate::Result;
+
+/// Wrapper around `checked_add()` that converts failures
+/// to `MmapError::Overflow`.
+pub trait CheckedOps: Sized {
+    fn add_chk(self, rhs: Self) -> Result<Self>;
+    fn mul_chk(self, rhs: Self) -> Result<Self>;
+}
+
+impl CheckedOps for usize {
+    fn add_chk(self, rhs: Self) -> Result<Self> {
+        self.checked_add(rhs)
+            .ok_or_else(|| MmapError::overflowed(self, rhs, "adding"))
+    }
+
+    fn mul_chk(self, rhs: Self) -> Result<Self> {
+        self.checked_mul(rhs)
+            .ok_or_else(|| MmapError::overflowed(self, rhs, "multiplying"))
+    }
+}
+
+impl CheckedOps for c_long {
+    fn add_chk(self, rhs: Self) -> Result<Self> {
+        self.checked_add(rhs)
+            .ok_or_else(|| MmapError::overflowed(self, rhs, "adding"))
+    }
+
+    fn mul_chk(self, rhs: Self) -> Result<Self> {
+        self.checked_mul(rhs)
+            .ok_or_else(|| MmapError::overflowed(self, rhs, "multiplying"))
+    }
+}
+
+/// A wrapper around `TryFrom`, returning `MmapError::FailedCast` on error.
+pub fn cast_chk<T, U>(val: T, name: &str) -> Result<U>
+where
+    T: Copy + Display,
+    U: std::convert::TryFrom<T>,
+{
+    U::try_from(val).map_err(|_| MmapError::failed_cast::<T, U>(val, name))
+}
+
+/// Retrieve errno(3).
+pub fn errno() -> i32 {
+    // UNWRAP: This will always return `Some` when called from `last_os_error()`.
+    io::Error::last_os_error().raw_os_error().unwrap()
+}
+
+/// Get the error string associated with errno(3).
+/// Equivalent to strerror(3).
+pub fn strerror(errno: i32) -> &'static str {
+    Errno::from_i32(errno).desc()
+}
+
+/// Read a `u32` value from a byte slice starting from `offset`.
+#[inline]
+pub fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
+    if let Some(slice) = buf.get(offset..offset + size_of::<u32>()) {
+        // UNWRAP: We can safely unwrap the conversion from slice to array as we
+        // the source and targets are constructed here with the same length.
+        let out: &[u8; size_of::<u32>()] = slice.try_into().unwrap();
+
+        return Ok(u32::from_ne_bytes(*out));
+    }
+    Err(MmapError::out_of_bounds(offset, buf.len()))
+}
+
+/// Read an `f64` value from a byte slice starting from `offset`.
+#[inline]
+pub fn read_f64(buf: &[u8], offset: usize) -> Result<f64> {
+    if let Some(slice) = buf.get(offset..offset + size_of::<f64>()) {
+        // UNWRAP: We can safely unwrap the conversion from slice to array as we
+        // can be sure the target array has same length as the source slice.
+        let out: &[u8; size_of::<f64>()] = slice.try_into().unwrap();
+
+        return Ok(f64::from_ne_bytes(*out));
+    }
+    Err(MmapError::out_of_bounds(
+        offset + size_of::<f64>(),
+        buf.len(),
+    ))
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_read_u32() {
+        let buf = 1u32.to_ne_bytes();
+
+        assert!(matches!(read_u32(&buf, 0), Ok(1)), "index ok");
+        assert!(read_u32(&buf, 10).is_err(), "index out of range");
+        assert!(
+            read_u32(&buf, 1).is_err(),
+            "index in range but end out of range"
+        );
+    }
+
+    #[test]
+    fn test_read_f64() {
+        let buf = 1.00f64.to_ne_bytes();
+
+        let ok = read_f64(&buf, 0);
+        assert!(ok.is_ok());
+        assert_eq!(ok.unwrap(), 1.00);
+
+        assert!(read_f64(&buf, 10).is_err(), "index out of range");
+        assert!(
+            read_f64(&buf, 1).is_err(),
+            "index in range but end out of range"
+        );
+    }
+}
diff --git a/lib/prometheus/client/configuration.rb b/lib/prometheus/client/configuration.rb
index 3ef8206..de5e827 100644
--- a/lib/prometheus/client/configuration.rb
+++ b/lib/prometheus/client/configuration.rb
@@ -1,18 +1,20 @@
 require 'prometheus/client/registry'
 require 'prometheus/client/mmaped_value'
+require 'prometheus/client/page_size'
 require 'logger'
 require 'tmpdir'
 
 module Prometheus
   module Client
     class Configuration
-      attr_accessor :value_class, :multiprocess_files_dir, :initial_mmap_file_size, :logger, :pid_provider
+      attr_accessor :value_class, :multiprocess_files_dir, :initial_mmap_file_size, :logger, :pid_provider, :rust_multiprocess_metrics
 
       def initialize
         @value_class = ::Prometheus::Client::MmapedValue
-        @initial_mmap_file_size = 4 * 1024
+        @initial_mmap_file_size = ::Prometheus::Client::PageSize.page_size(fallback_page_size: 4096)
         @logger = Logger.new($stdout)
         @pid_provider = Process.method(:pid)
+        @rust_multiprocess_metrics = ENV.fetch('prometheus_rust_multiprocess_metrics', nil) == 'true'
         @multiprocess_files_dir = ENV.fetch('prometheus_multiproc_dir') do
           Dir.mktmpdir("prometheus-mmap")
         end
diff --git a/lib/prometheus/client/formats/text.rb b/lib/prometheus/client/formats/text.rb
index b61057d..a3139e0 100644
--- a/lib/prometheus/client/formats/text.rb
+++ b/lib/prometheus/client/formats/text.rb
@@ -1,5 +1,6 @@
 require 'prometheus/client/uses_value_type'
 require 'prometheus/client/helper/json_parser'
+require 'prometheus/client/helper/loader'
 require 'prometheus/client/helper/plain_file'
 require 'prometheus/client/helper/metrics_processing'
 require 'prometheus/client/helper/metrics_representation'
@@ -26,12 +27,22 @@ module Prometheus
             Helper::MetricsRepresentation.to_text(metrics)
           end
 
-          def marshal_multiprocess(path = Prometheus::Client.configuration.multiprocess_files_dir)
+          def marshal_multiprocess(path = Prometheus::Client.configuration.multiprocess_files_dir, use_rust: false)
             file_list = Dir.glob(File.join(path, '*.db')).sort
               .map {|f| Helper::PlainFile.new(f) }
               .map {|f| [f.filepath, f.multiprocess_mode.to_sym, f.type.to_sym, f.pid] }
 
-            FastMmapedFile.to_metrics(file_list.to_a)
+            if use_rust && Prometheus::Client::Helper::Loader.rust_impl_available?
+              FastMmapedFileRs.to_metrics(file_list.to_a)
+            else
+              FastMmapedFile.to_metrics(file_list.to_a)
+            end
+          end
+
+          def rust_impl_available?
+            return @rust_available unless @rust_available.nil?
+
+            check_for_rust
           end
 
           private
diff --git a/lib/prometheus/client/helper/loader.rb b/lib/prometheus/client/helper/loader.rb
new file mode 100644
index 0000000..e642d5a
--- /dev/null
+++ b/lib/prometheus/client/helper/loader.rb
@@ -0,0 +1,40 @@
+module Prometheus
+  module Client
+    module Helper
+      module Loader
+        class << self
+          def rust_impl_available?
+            return @rust_available unless @rust_available.nil?
+
+            check_for_rust
+          end
+
+          private
+
+          def load_rust_extension
+            begin
+              ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
+              require_relative "../../../#{ruby_version}/fast_mmaped_file_rs"
+            rescue LoadError
+              require 'fast_mmaped_file_rs'
+            end
+          end
+
+          def check_for_rust
+            # This will be evaluated on each invocation even with `||=` if
+            # `@rust_available` if false. Running a `require` statement is slow,
+            # so the `rust_impl_available?` method memoizes the result, external
+            # callers can only trigger this method a single time.
+            @rust_available = begin
+              load_rust_extension
+              true
+            rescue LoadError
+              Prometheus::Client.logger.info('FastMmapedFileRs unavailable')
+              false
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/prometheus/client/helper/mmaped_file.rb b/lib/prometheus/client/helper/mmaped_file.rb
index ba08122..86fb1fc 100644
--- a/lib/prometheus/client/helper/mmaped_file.rb
+++ b/lib/prometheus/client/helper/mmaped_file.rb
@@ -1,11 +1,29 @@
 require 'prometheus/client/helper/entry_parser'
 require 'prometheus/client/helper/file_locker'
-require 'fast_mmaped_file'
+require 'prometheus/client/helper/loader'
+
+# load precompiled extension if available
+begin
+  ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
+  require_relative "../../../#{ruby_version}/fast_mmaped_file"
+rescue LoadError
+  require 'fast_mmaped_file'
+end
 
 module Prometheus
   module Client
     module Helper
-      class MmapedFile < FastMmapedFile
+      # We can't check `Prometheus::Client.configuration` as this creates a circular dependency
+      if (ENV.fetch('prometheus_rust_mmaped_file', nil) == "true" &&
+          Prometheus::Client::Helper::Loader.rust_impl_available?)
+        class MmapedFile < FastMmapedFileRs
+        end
+      else
+        class MmapedFile < FastMmapedFile
+        end
+      end
+
+      class MmapedFile
         include EntryParser
 
         attr_reader :filepath, :size
diff --git a/lib/prometheus/client/page_size.rb b/lib/prometheus/client/page_size.rb
new file mode 100644
index 0000000..494185a
--- /dev/null
+++ b/lib/prometheus/client/page_size.rb
@@ -0,0 +1,17 @@
+require 'open3'
+
+module Prometheus
+  module Client
+    module PageSize
+      def self.page_size(fallback_page_size: 4096)
+        stdout, status = Open3.capture2('getconf PAGESIZE')
+        return fallback_page_size if status.nil? || !status.success?
+
+        page_size = stdout.chomp.to_i
+        return fallback_page_size if page_size <= 0
+
+        page_size
+      end
+    end
+  end
+end
diff --git a/lib/prometheus/client/rack/exporter.rb b/lib/prometheus/client/rack/exporter.rb
index 593ba60..e5cffcd 100644
--- a/lib/prometheus/client/rack/exporter.rb
+++ b/lib/prometheus/client/rack/exporter.rb
@@ -62,8 +62,10 @@ module Prometheus
         end
 
         def respond_with(format)
+          rust_enabled = Prometheus::Client.configuration.rust_multiprocess_metrics
+
           response = if Prometheus::Client.configuration.value_class.multiprocess
-                       format.marshal_multiprocess
+                       format.marshal_multiprocess(use_rust: rust_enabled)
                      else
                        format.marshal
                      end
diff --git a/lib/prometheus/client/version.rb b/lib/prometheus/client/version.rb
index 9dbe41c..2010717 100644
--- a/lib/prometheus/client/version.rb
+++ b/lib/prometheus/client/version.rb
@@ -1,5 +1,5 @@
 module Prometheus
   module Client
-    VERSION = '0.19.1'.freeze
+    VERSION = '0.24.3'.freeze
   end
 end
diff --git a/prometheus-client-mmap.gemspec b/prometheus-client-mmap.gemspec
index 3620cc8..44f53a3 100644
--- a/prometheus-client-mmap.gemspec
+++ b/prometheus-client-mmap.gemspec
@@ -7,21 +7,24 @@ Gem::Specification.new do |s|
   s.version           = Prometheus::Client::VERSION
   s.summary           = 'A suite of instrumentation metric primitives' \
                         'that can be exposed through a web services interface.'
-  s.authors           = ['Tobias Schmidt', 'Paweł Chojnacki']
-  s.email             = ['ts@soundcloud.com', 'pawel@gitlab.com']
+  s.authors           = ['Tobias Schmidt', 'Paweł Chojnacki', 'Stan Hu', 'Will Chandler']
+  s.email             = ['ts@soundcloud.com', 'pawel@gitlab.com', 'stanhu@gmail.com', 'wchandler@gitlab.com']
   s.homepage          = 'https://gitlab.com/gitlab-org/prometheus-client-mmap'
   s.license           = 'Apache-2.0'
 
-  s.files             = %w(README.md) + Dir.glob('{lib/**/*}') + Dir.glob('{ext/**/*}') + Dir.glob('{vendor/**/*}')
+  s.files             = `git ls-files README.md .tool-versions lib ext vendor`.split("\n")
   s.require_paths     = ['lib']
   s.extensions        = Dir.glob('{ext/**/extconf.rb}')
 
   # This C extension uses ObjectSpace::WeakRef with Integer keys (https://bugs.ruby-lang.org/issues/16035)
   s.required_ruby_version = '>= 2.7.0'
 
+  s.add_dependency "rb_sys", "~> 0.9"
+
   s.add_development_dependency 'fuzzbert', '~> 1.0', '>= 1.0.4'
   s.add_development_dependency 'gem_publisher', '~> 1'
   s.add_development_dependency 'pry', '~> 0.12.2'
-  s.add_development_dependency 'rake-compiler', '~> 1'
+  s.add_development_dependency "rake-compiler", "~> 1.2.1"
+  s.add_development_dependency "rake-compiler-dock", "~> 1.3.0"
   s.add_development_dependency 'ruby-prof', '~> 0.16.2'
 end
diff --git a/spec/prometheus/client/formats/text_spec.rb b/spec/prometheus/client/formats/text_spec.rb
index c87da37..c8f9cb8 100644
--- a/spec/prometheus/client/formats/text_spec.rb
+++ b/spec/prometheus/client/formats/text_spec.rb
@@ -112,134 +112,139 @@ describe Prometheus::Client::Formats::Text do
   end
 
   context 'multi process metrics', :temp_metrics_dir do
-    let(:registry) { Prometheus::Client::Registry.new }
+    [true, false].each do |use_rust|
+      context "when rust_multiprocess_metrics is #{use_rust}" do
+        let(:registry) { Prometheus::Client::Registry.new }
 
-    before do
-      allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(temp_metrics_dir)
-      # reset all current metrics
-      Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
-    end
+        before do
+          allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(temp_metrics_dir)
+          allow(Prometheus::Client.configuration).to receive(:rust_multiprocess_metrics).and_return(use_rust)
+          # reset all current metrics
+          Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
+        end
 
-    context 'pid provider returns compound ID', :temp_metrics_dir, :sample_metrics do
-      before do
-        allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { 'pid_provider_id_1' })
-        # Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
-        add_simple_metrics(registry)
-      end
+        context 'pid provider returns compound ID', :temp_metrics_dir, :sample_metrics do
+          before do
+            allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { 'pid_provider_id_1' })
+            # Prometheus::Client::MmapedValue.class_variable_set(:@@files, {})
+            add_simple_metrics(registry)
+          end
 
-      it '.marshal_multiprocess' do
-        expect(described_class.marshal_multiprocess(temp_metrics_dir)).to eq <<-'TEXT'.gsub(/^\s+/, '')
-          # HELP counter Multiprocess metric
-          # TYPE counter counter
-          counter{a="1",b="1"} 1
-          counter{a="1",b="2"} 1
-          counter{a="2",b="1"} 1
-          # HELP gauge Multiprocess metric
-          # TYPE gauge gauge
-          gauge{b="1"} 1
-          gauge{b="2"} 1
-          # HELP gauge_with_big_value Multiprocess metric
-          # TYPE gauge_with_big_value gauge
-          gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
-          gauge_with_big_value{a="12345678901234567"} 12345678901234568
-          # HELP gauge_with_null_labels Multiprocess metric
-          # TYPE gauge_with_null_labels gauge
-          gauge_with_null_labels{a="",b=""} 1
-          # HELP gauge_with_pid Multiprocess metric
-          # TYPE gauge_with_pid gauge
-          gauge_with_pid{b="1",c="1",pid="pid_provider_id_1"} 1
-          # HELP histogram Multiprocess metric
-          # TYPE histogram histogram
-          histogram_bucket{a="1",le="+Inf"} 1
-          histogram_bucket{a="1",le="0.005"} 0
-          histogram_bucket{a="1",le="0.01"} 0
-          histogram_bucket{a="1",le="0.025"} 0
-          histogram_bucket{a="1",le="0.05"} 0
-          histogram_bucket{a="1",le="0.1"} 0
-          histogram_bucket{a="1",le="0.25"} 0
-          histogram_bucket{a="1",le="0.5"} 0
-          histogram_bucket{a="1",le="1"} 1
-          histogram_bucket{a="1",le="10"} 1
-          histogram_bucket{a="1",le="2.5"} 1
-          histogram_bucket{a="1",le="5"} 1
-          histogram_count{a="1"} 1
-          histogram_sum{a="1"} 1
-          # HELP summary Multiprocess metric
-          # TYPE summary summary
-          summary_count{a="1",b="1"} 1
-          summary_sum{a="1",b="1"} 1
-        TEXT
-      end
-    end
+          it '.marshal_multiprocess' do
+            expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: true)).to eq <<-'TEXT'.gsub(/^\s+/, '')
+              # HELP counter Multiprocess metric
+              # TYPE counter counter
+              counter{a="1",b="1"} 1
+              counter{a="1",b="2"} 1
+              counter{a="2",b="1"} 1
+              # HELP gauge Multiprocess metric
+              # TYPE gauge gauge
+              gauge{b="1"} 1
+              gauge{b="2"} 1
+              # HELP gauge_with_big_value Multiprocess metric
+              # TYPE gauge_with_big_value gauge
+              gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
+              gauge_with_big_value{a="12345678901234567"} 12345678901234568
+              # HELP gauge_with_null_labels Multiprocess metric
+              # TYPE gauge_with_null_labels gauge
+              gauge_with_null_labels{a="",b=""} 1
+              # HELP gauge_with_pid Multiprocess metric
+              # TYPE gauge_with_pid gauge
+              gauge_with_pid{b="1",c="1",pid="pid_provider_id_1"} 1
+              # HELP histogram Multiprocess metric
+              # TYPE histogram histogram
+              histogram_bucket{a="1",le="+Inf"} 1
+              histogram_bucket{a="1",le="0.005"} 0
+              histogram_bucket{a="1",le="0.01"} 0
+              histogram_bucket{a="1",le="0.025"} 0
+              histogram_bucket{a="1",le="0.05"} 0
+              histogram_bucket{a="1",le="0.1"} 0
+              histogram_bucket{a="1",le="0.25"} 0
+              histogram_bucket{a="1",le="0.5"} 0
+              histogram_bucket{a="1",le="1"} 1
+              histogram_bucket{a="1",le="10"} 1
+              histogram_bucket{a="1",le="2.5"} 1
+              histogram_bucket{a="1",le="5"} 1
+              histogram_count{a="1"} 1
+              histogram_sum{a="1"} 1
+              # HELP summary Multiprocess metric
+              # TYPE summary summary
+              summary_count{a="1",b="1"} 1
+              summary_sum{a="1",b="1"} 1
+            TEXT
+          end
+        end
 
-    context 'pid provider returns numerical value', :temp_metrics_dir, :sample_metrics do
-      before do
-        allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { -1 })
-        add_simple_metrics(registry)
-      end
+        context 'pid provider returns numerical value', :temp_metrics_dir, :sample_metrics do
+          before do
+            allow(Prometheus::Client.configuration).to receive(:pid_provider).and_return(-> { -1 })
+            add_simple_metrics(registry)
+          end
 
-      it '.marshal_multiprocess' do
-        expect(described_class.marshal_multiprocess(temp_metrics_dir)).to eq <<-'TEXT'.gsub(/^\s+/, '')
-          # HELP counter Multiprocess metric
-          # TYPE counter counter
-          counter{a="1",b="1"} 1
-          counter{a="1",b="2"} 1
-          counter{a="2",b="1"} 1
-          # HELP gauge Multiprocess metric
-          # TYPE gauge gauge
-          gauge{b="1"} 1
-          gauge{b="2"} 1
-          # HELP gauge_with_big_value Multiprocess metric
-          # TYPE gauge_with_big_value gauge
-          gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
-          gauge_with_big_value{a="12345678901234567"} 12345678901234568
-          # HELP gauge_with_null_labels Multiprocess metric
-          # TYPE gauge_with_null_labels gauge
-          gauge_with_null_labels{a="",b=""} 1
-          # HELP gauge_with_pid Multiprocess metric
-          # TYPE gauge_with_pid gauge
-          gauge_with_pid{b="1",c="1",pid="-1"} 1
-          # HELP histogram Multiprocess metric
-          # TYPE histogram histogram
-          histogram_bucket{a="1",le="+Inf"} 1
-          histogram_bucket{a="1",le="0.005"} 0
-          histogram_bucket{a="1",le="0.01"} 0
-          histogram_bucket{a="1",le="0.025"} 0
-          histogram_bucket{a="1",le="0.05"} 0
-          histogram_bucket{a="1",le="0.1"} 0
-          histogram_bucket{a="1",le="0.25"} 0
-          histogram_bucket{a="1",le="0.5"} 0
-          histogram_bucket{a="1",le="1"} 1
-          histogram_bucket{a="1",le="10"} 1
-          histogram_bucket{a="1",le="2.5"} 1
-          histogram_bucket{a="1",le="5"} 1
-          histogram_count{a="1"} 1
-          histogram_sum{a="1"} 1
-          # HELP summary Multiprocess metric
-          # TYPE summary summary
-          summary_count{a="1",b="1"} 1
-          summary_sum{a="1",b="1"} 1
-        TEXT
-      end
-    end
+          it '.marshal_multiprocess' do
+            expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: use_rust)).to eq <<-'TEXT'.gsub(/^\s+/, '')
+              # HELP counter Multiprocess metric
+              # TYPE counter counter
+              counter{a="1",b="1"} 1
+              counter{a="1",b="2"} 1
+              counter{a="2",b="1"} 1
+              # HELP gauge Multiprocess metric
+              # TYPE gauge gauge
+              gauge{b="1"} 1
+              gauge{b="2"} 1
+              # HELP gauge_with_big_value Multiprocess metric
+              # TYPE gauge_with_big_value gauge
+              gauge_with_big_value{a="0.12345678901234566"} 0.12345678901234566
+              gauge_with_big_value{a="12345678901234567"} 12345678901234568
+              # HELP gauge_with_null_labels Multiprocess metric
+              # TYPE gauge_with_null_labels gauge
+              gauge_with_null_labels{a="",b=""} 1
+              # HELP gauge_with_pid Multiprocess metric
+              # TYPE gauge_with_pid gauge
+              gauge_with_pid{b="1",c="1",pid="-1"} 1
+              # HELP histogram Multiprocess metric
+              # TYPE histogram histogram
+              histogram_bucket{a="1",le="+Inf"} 1
+              histogram_bucket{a="1",le="0.005"} 0
+              histogram_bucket{a="1",le="0.01"} 0
+              histogram_bucket{a="1",le="0.025"} 0
+              histogram_bucket{a="1",le="0.05"} 0
+              histogram_bucket{a="1",le="0.1"} 0
+              histogram_bucket{a="1",le="0.25"} 0
+              histogram_bucket{a="1",le="0.5"} 0
+              histogram_bucket{a="1",le="1"} 1
+              histogram_bucket{a="1",le="10"} 1
+              histogram_bucket{a="1",le="2.5"} 1
+              histogram_bucket{a="1",le="5"} 1
+              histogram_count{a="1"} 1
+              histogram_sum{a="1"} 1
+              # HELP summary Multiprocess metric
+              # TYPE summary summary
+              summary_count{a="1",b="1"} 1
+              summary_sum{a="1",b="1"} 1
+            TEXT
+          end
+        end
 
-    context 'when OJ is available uses OJ to parse keys' do
-      let(:oj) { double(oj) }
-      before do
-        stub_const 'Oj', oj
-        allow(oj).to receive(:load)
-      end
-    end
+        context 'when OJ is available uses OJ to parse keys' do
+          let(:oj) { double(oj) }
+          before do
+            stub_const 'Oj', oj
+            allow(oj).to receive(:load)
+          end
+        end
 
-    context 'with metric having whitespace and UTF chars', :temp_metrics_dir do
-      before do
-        registry.gauge(:gauge, "bar description\nwith newline", { umlauts: 'Björn', utf: '佖佥' }, :all).set({ umlauts: 'Björn', utf: '佖佥' }, 1)
-      end
+        context 'with metric having whitespace and UTF chars', :temp_metrics_dir do
+          before do
+            registry.gauge(:gauge, "bar description\nwith newline", { umlauts: 'Björn', utf: '佖佥' }, :all).set({ umlauts: 'Björn', utf: '佖佥' }, 1)
+          end
 
-      xit '.marshall_multiprocess' do
-        expect(described_class.marshal_multiprocess(temp_metrics_dir)).to eq <<-'TEXT'.gsub(/^\s+/, '')
-TODO...
-        TEXT
+          xit '.marshall_multiprocess' do
+            expect(described_class.marshal_multiprocess(temp_metrics_dir, use_rust: true)).to eq <<-'TEXT'.gsub(/^\s+/, '')
+    TODO...
+            TEXT
+          end
+        end
       end
     end
   end
diff --git a/spec/prometheus/client/helpers/mmaped_file_spec.rb b/spec/prometheus/client/helpers/mmaped_file_spec.rb
index 4f61939..da51f7a 100644
--- a/spec/prometheus/client/helpers/mmaped_file_spec.rb
+++ b/spec/prometheus/client/helpers/mmaped_file_spec.rb
@@ -1,5 +1,6 @@
 require 'spec_helper'
 require 'prometheus/client/helper/mmaped_file'
+require 'prometheus/client/page_size'
 
 describe Prometheus::Client::Helper::MmapedFile do
   let(:filename) { Dir::Tmpname.create('mmaped_file_') {} }
@@ -31,14 +32,15 @@ describe Prometheus::Client::Helper::MmapedFile do
     end
 
     context 'when initial mmap size is larger' do
-      let (:initial_mmap_file_size) { 9999 }
+      let(:page_size) { Prometheus::Client::PageSize.page_size }
+      let (:initial_mmap_file_size) { page_size + 1024 }
 
       before do
         allow_any_instance_of(described_class).to receive(:initial_mmap_file_size).and_return(initial_mmap_file_size)
       end
 
       it 'creates a file with increased minimum initial size' do
-        expect(File.size(subject.filepath)).to eq(16384);
+        expect(File.size(subject.filepath)).to eq(page_size * 2);
       end
     end
   end
diff --git a/spec/prometheus/client/mmaped_dict_spec.rb b/spec/prometheus/client/mmaped_dict_spec.rb
index bed9688..25e8d3a 100644
--- a/spec/prometheus/client/mmaped_dict_spec.rb
+++ b/spec/prometheus/client/mmaped_dict_spec.rb
@@ -1,4 +1,5 @@
 require 'prometheus/client/mmaped_dict'
+require 'prometheus/client/page_size'
 require 'tempfile'
 
 describe Prometheus::Client::MmapedDict do
@@ -22,6 +23,7 @@ describe Prometheus::Client::MmapedDict do
 
     describe "mmap'ed file that is above minimum size" do
       let(:above_minimum_size) { Prometheus::Client::Helper::EntryParser::MINIMUM_SIZE + 1 }
+      let(:page_size) { Prometheus::Client::PageSize.page_size }
 
       before do
         tmp_file.truncate(above_minimum_size)
@@ -31,7 +33,7 @@ describe Prometheus::Client::MmapedDict do
         described_class.new(tmp_mmaped_file)
 
         tmp_file.open
-        expect(tmp_file.size).to eq(4096);
+        expect(tmp_file.size).to eq(page_size);
       end
     end
   end
@@ -39,8 +41,9 @@ describe Prometheus::Client::MmapedDict do
   describe 'read on boundary conditions' do
     let(:locked_file) { Prometheus::Client::Helper::MmapedFile.ensure_exclusive_file }
     let(:mmaped_file) { Prometheus::Client::Helper::MmapedFile.open(locked_file) }
-    let(:target_size) { 4096 }
-    let(:iterations) { 4096 / 32 }
+    let(:page_size) { Prometheus::Client::PageSize.page_size }
+    let(:target_size) { page_size }
+    let(:iterations) { page_size / 32 }
     let(:dummy_key) { '1234' }
     let(:dummy_value) { 1.0 }
     let(:expected) { { dummy_key => dummy_value } }
@@ -54,8 +57,9 @@ describe Prometheus::Client::MmapedDict do
       # To generate a file like this, we create entries that require 32 bytes
       # total to store with 7 bytes of padding at the end.
       #
-      # To make things align evenly against 4096 bytes, add a dummy entry that will occupy the
-      # next 3 bytes to start on a 32-byte boundary. The file structure looks like:
+      # To make things align evenly against the system page size, add a dummy
+      # entry that will occupy the next 3 bytes to start on a 32-byte boundary.
+      # The filestructure looks like:
       #
       # Bytes 0-3  : Total used size of file
       # Bytes 4-7  : Padding
diff --git a/spec/prometheus/client/mmaped_value_spec.rb b/spec/prometheus/client/mmaped_value_spec.rb
index e4a02d8..1cde042 100644
--- a/spec/prometheus/client/mmaped_value_spec.rb
+++ b/spec/prometheus/client/mmaped_value_spec.rb
@@ -1,4 +1,5 @@
 require 'prometheus/client/mmaped_dict'
+require 'prometheus/client/page_size'
 require 'tempfile'
 
 describe Prometheus::Client::MmapedValue, :temp_metrics_dir do
@@ -62,6 +63,7 @@ describe Prometheus::Client::MmapedValue, :temp_metrics_dir do
 
       describe 'PID changed' do
         let(:new_pid) { pid - 1 }
+        let(:page_size) { Prometheus::Client::PageSize.page_size }
 
         before do
           counter.increment
@@ -123,8 +125,8 @@ describe Prometheus::Client::MmapedValue, :temp_metrics_dir do
           expect(counter.get).not_to eq(@old_value)
         end
 
-        it 'updates strings properly upon memory expansion' do
-          described_class.new(:gauge, :gauge, 'gauge2', { label_1: 'x' * 1024 * 8 }, :all)
+        it 'updates strings properly upon memory expansion', :page_size do
+          described_class.new(:gauge, :gauge, 'gauge2', { label_1: 'x' * page_size * 2 }, :all)
 
           # This previously failed on Linux but not on macOS since mmap() may re-allocate the same region.
           ObjectSpace.each_object(String, &:valid_encoding?)
diff --git a/tools/deploy-rubygem.sh b/tools/deploy-rubygem.sh
new file mode 100755
index 0000000..ea4bdee
--- /dev/null
+++ b/tools/deploy-rubygem.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+IFS=$'\n\t'
+
+mkdir -p ~/.gem/
+temp_file=$(mktemp)
+
+# Revert change the credentials file
+function revert_gem_cred_switch {
+  mv "$temp_file" ~/.gem/credentials
+}
+
+if [[ -f ~/.gem/credentials ]]; then
+  echo "Temporarily moving existing credentials to $temp_file"
+  mv ~/.gem/credentials "$temp_file"
+  trap revert_gem_cred_switch EXIT
+fi
+
+cat <<EOD > ~/.gem/credentials
+---
+:rubygems_api_key: $RUBYGEMS_API_KEY
+EOD
+chmod 600 ~/.gem/credentials
+
+set -x
+
+bundle install
+bundle exec rake build
+ls pkg/*.gem | xargs -n 1 gem push

More details

Full run details

Historical runs