Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
- name: Install Zig
run: |
ZIG_ARCH=$(uname -m | sed 's/arm64/aarch64/')
ZIG_VERSION="0.15.2"
ZIG_VERSION="0.16.0"
curl -sSfL "https://ziglang.org/download/${ZIG_VERSION}/zig-${ZIG_ARCH}-macos-${ZIG_VERSION}.tar.xz" \
| tar xJ -C /tmp
echo "/tmp/zig-${ZIG_ARCH}-macos-${ZIG_VERSION}" >> $GITHUB_PATH
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: Install Zig
if: ${{ !matrix.fallback }}
run: |
ZIG_VERSION="0.15.2"
ZIG_VERSION="0.16.0"
ZIG_ARCH=$(uname -m | sed 's/arm64/aarch64/')
# Zig uses "macos" in download slugs, not "darwin" as uname reports.
ZIG_OS=$(uname -s | tr '[:upper:]' '[:lower:]' | sed 's/darwin/macos/')
Expand Down
4 changes: 2 additions & 2 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[tools]
ruby = ["4.0.2", "3.4.8"]
zig = "0.15.2"
ruby = ["4.0.3", "3.4.9"]
zig = "0.16.0"
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [Unreleased]

- Update to Zig 0.16.0; update libxev and zig.rb to the latest versions.
- Performance improvements. README updated with latest numbers.
- Improve benchmarking: default to 3 measured runs plus 1 warmup run. Now reporting `±CV%` and the delta column tags rows `(noisy)` when the change is within jitter. Add the ability to benchmark released vs. unreleased carbon_fiber.

## [0.1.2] - 2026-04-29

- Fix the `x86_64-linux` precompiled gem (erroneously shipped an aarch64
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ GEM
prism (1.9.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.1)
rake (13.4.2)
rake-compiler-dock (1.11.1)
regexp_parser (2.12.0)
rspec (3.13.2)
Expand Down Expand Up @@ -124,7 +124,7 @@ CHECKSUMS
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
rake-compiler-dock (1.11.1) sha256=9234ded6c3311f4ba6db77f2833f0aa33c073b4d33c896a2156e2551af828e0a
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ end

## Performance

AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Ubuntu 24.04 LTS, kernel 6.17, Ruby 4.0.2 + YJIT, io_uring. 5-run median.
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Amazon Linux 2023, kernel 6.18.20, Ruby 4.0.2 + YJIT, io_uring. 5-run median.

Some benchmarks:

| Workload | Carbon Fiber | Async | Itsi | Carbon Fiber vs. Async |
|---|---|---|---|---|
| `http_server` | **48.175k req/s** | 37.409k req/s | 29.708k req/s | +29% |
| `http_client_api` | **15.528k req/s** | 13.373k req/s | timeout | +16% |
| `http_client_download` | **6.747k dl/s** | 5.920k dl/s | timeout | +14% |
| `tcp_echo` | **50.392k ops/s** | 38.907k ops/s | 30.660k ops/s | +29% |
| `cascading_timeout` | **4.659k ops/s** | 4.488k ops/s | error | +4% |
| `connection_pool` | **4.989k co/s** | 4.912k co/s | 4.968k co/s | +2% |
| `http_server` | **49.380k req/s** | 30.823k req/s | 30.864k req/s | +60% |
| `http_client_api` | **19.500k req/s** | 16.721k req/s | timeout | +17% |
| `http_client_download` | **8.426k dl/s** | 7.062k dl/s | timeout | +19% |
| `tcp_echo` | **52.973k ops/s** | 32.330k ops/s | 32.046k ops/s | +64% |
| `cascading_timeout` | **4.668k ops/s** | 4.414k ops/s | error | +6% |
| `connection_pool` | **4.967k co/s** | 4.612k co/s | 4.954k co/s | +8% |

Wins on most workloads against Async, Itsi, fiber_scheduler, io-event, and libev. [See detailed benchmarks →](#benchmarks)

Expand Down Expand Up @@ -256,22 +256,22 @@ If the native extension can't be loaded (on Windows, for example), a pure-Ruby f

## Benchmarks

AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Ubuntu 24.04 LTS, kernel 6.17, Ruby 4.0.2 + YJIT, io_uring. 5-run median.
AWS EC2 c7a.2xlarge, 8 dedicated vCPUs, Amazon Linux 2023, kernel 6.18.20, Ruby 4.0.2 + YJIT, io_uring. 5-run median.

### Ruby Fiber Schedulers (leading ones): Carbon Fiber vs. Async vs. Itsi

Measuring pure Ruby Fiber Scheduler performance (`Fiber.set_scheduler`).

| Workload | Unit | Carbon Fiber | Async | Itsi | Carbon Fiber vs. Async |
|---|---|---|---|---|---|
| `http_client_api` | req/s | **15,528** | 13,373 | timeout | +16% |
| `http_client_download` | dl/s | **6,747** | 5,920 | timeout | +14% |
| `http_server` | req/s | **48,175** | 37,409 | 29,708 | +29% |
| `tcp_echo` | ops/s | **50,392** | 38,907 | 30,660 | +29% |
| `connection_pool` | co/s | **4,989** | 4,912 | 4,968 | +2% |
| `fan_out_gather` | cyc/s | 2,024 | 2,046 | **2,104** | −1% |
| `db_query_mix` | qry/s | 1,660 | 1,652 | **1,662** | +0.5% |
| `cascading_timeout` | ops/s | **4,659** | 4,488 | error | +4% |
| `http_client_api` | req/s | **19,500** | 16,721 | timeout | +17% |
| `http_client_download` | dl/s | **8,426** | 7,062 | timeout | +19% |
| `http_server` | req/s | **49,380** | 30,823 | 30,864 | +60% |
| `tcp_echo` | ops/s | **52,973** | 32,330 | 32,046 | +64% |
| `connection_pool` | co/s | **4,967** | 4,612 | 4,954 | +8% |
| `fan_out_gather` | cyc/s | 2,022 | 1,923 | **2,094** | +5% |
| `db_query_mix` | qry/s | 1,660 | 1,623 | **1,662** | +2% |
| `cascading_timeout` | ops/s | **4,668** | 4,414 | error | +6% |

Enabling YJIT turned out to be very beneficial for Async as well—numbers here are with `--yjit` on both sides.

Expand All @@ -281,12 +281,12 @@ Swapped the io-event selector for Carbon Fiber's native backend. Same Async code

| Workload | Unit | Stock Async | Carbon Fiber | Delta |
|---|---|---|---|---|
| `http_client_api` | req/s | 13,375 | **14,331** | +7.1% |
| `http_client_download` | dl/s | 3,893 | **3,956** | +1.6% |
| `task_churn` | task/s | **87,883** | 85,027 | −3.3% |
| `condition_signal` | sig/s | 337,282 | **361,089** | +7.1% |
| `cascading_timeout` | ops/s | 4,497 | **4,511** | +0.3% |
| `tcp_throughput` | ops/s | 42,292 | **51,930** | +22.8% |
| `http_client_api` | req/s | 17,119 | **17,323** | +1.2% |
| `http_client_download` | dl/s | 2,263 | **2,474** | +9.3% |
| `task_churn` | task/s | 119,477 | **120,985** | +1.3% |
| `condition_signal` | sig/s | 44,887 | **46,921** | +4.5% |
| `cascading_timeout` | ops/s | 4,414 | **4,472** | +1.3% |
| `tcp_throughput` | ops/s | 32,606 | **48,214** | +47.9% |

### Examples of how to run benchmarks

Expand Down
37 changes: 19 additions & 18 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,9 @@ task :sync_version do
end
end

# Zig 0.15's Mach-O linker has issues with Xcode 26.4
# - https://codeberg.org/ziglang/zig/pulls/31673
# - https://codeberg.org/ziglang/zig/issues/31658
# Workaround: DEVELOPER_DIR=/dev/null makes Zig use its bundled libSystem.tbd.
desc "Compile the Zig native extension (also syncs version)"
task compile: :sync_version do
env = RUBY_PLATFORM.include?("darwin") ? {"DEVELOPER_DIR" => "/dev/null"} : {}
sh env, "zig", "build", "-Doptimize=ReleaseFast"
sh "zig", "build", "-Doptimize=ReleaseFast"
fix_macos_install_names if RUBY_PLATFORM.include?("darwin")
end

Expand Down Expand Up @@ -57,12 +52,18 @@ def fix_macos_install_names
sh "install_name_tool", "-delete_rpath", rpath, bundle if rpath.start_with?("/")
end

# Add a portable rpath so the bundle can locate libruby on any host.
unless rpaths.include?("@executable_path/../lib")
sh "install_name_tool", "-add_rpath", "@executable_path/../lib", bundle
end

sh "codesign", "--sign", "-", "--force", bundle

final_libs = `otool -L #{bundle.shellescape}`
leaked = final_libs.lines.find do |l|
next false unless l.start_with?("\t/")
next false if l.match?(%r{^\t/usr/lib/}) || l.match?(%r{^\t/System/})

true
end
raise "Non-portable dylib in #{bundle}: #{leaked.strip}\n\n#{final_libs}" if leaked
Expand All @@ -73,26 +74,26 @@ def fix_macos_install_names
end
end

ZIG_VERSION = "0.15.2"
ZIG_VERSION = "0.16.0"

# Ruby versions to compile for. Full version must exactly match the directory
# name inside the RCD image: docker run --rm <image> ls /usr/local/rake-compiler/ruby/x86_64-linux-gnu/
RUBY_CROSS_VERSIONS = [
{full: "3.4.8", api: "3.4.0"},
{full: "4.0.0", api: "4.0.0"}
]
].freeze

# [gem_platform_name, rcd_platform (image suffix), zig_target_triple]
# rcd_platform also names the dir inside the RCD container *most* of the time;
# scripts/rcd_build.sh discovers the actual path because the x86_64-musl image
# uses x86_64-unknown-linux-musl as its top dir.
LINUX_PLATFORMS = [
["x86_64-linux", "x86_64-linux-gnu", "x86_64-linux-gnu"],
["aarch64-linux", "aarch64-linux-gnu", "aarch64-linux-gnu"],
["x86_64-linux-musl", "x86_64-linux-musl", "x86_64-linux-musl"],
["aarch64-linux-musl", "aarch64-linux-musl", "aarch64-linux-musl"]
]
DARWIN_PLATFORMS = %w[arm64-darwin]
%w[x86_64-linux x86_64-linux-gnu x86_64-linux-gnu],
%w[aarch64-linux aarch64-linux-gnu aarch64-linux-gnu],
%w[x86_64-linux-musl x86_64-linux-musl x86_64-linux-musl],
%w[aarch64-linux-musl aarch64-linux-musl aarch64-linux-musl]
].freeze
DARWIN_PLATFORMS = %w[arm64-darwin].freeze
ALL_PLATFORMS = LINUX_PLATFORMS.map(&:first) + DARWIN_PLATFORMS

# Cross-compile tasks
Expand All @@ -103,11 +104,11 @@ namespace :cross do
task gem_platform do
require "rake_compiler_dock"

build_cmds = RUBY_CROSS_VERSIONS.map { |r|
build_cmds = RUBY_CROSS_VERSIONS.map do |r|
"RUBY_FULL_VERSION=#{r[:full]} RUBY_API_VERSION=#{r[:api]} " \
"RCD_PLATFORM=#{rcd_platform} TARGET_TRIPLE=#{zig_triple} " \
"ZIG_VERSION=#{ZIG_VERSION} bash scripts/rcd_build.sh"
}.join(" && ")
end.join(" && ")

RakeCompilerDock.sh(build_cmds, platform: rcd_platform)
end
Expand All @@ -132,7 +133,7 @@ def build_platform_gem(platform)

spec = Gem::Specification.load("carbon_fiber.gemspec")
spec.platform = Gem::Platform.new(platform)
spec.extensions = [] # pre-built; no compilation on install
spec.extensions = [] # pre-built; no compilation on install

ext = platform.include?("darwin") ? "bundle" : "so"
files = Dir["lib/**/*.rb"].reject { |f| File.directory?(f) } +
Expand Down Expand Up @@ -210,4 +211,4 @@ task :lint do
end

desc "Compile native extension and run specs"
task default: [:compile, :spec]
task default: %i[compile spec]
2 changes: 1 addition & 1 deletion benchmarks/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM ruby:4.0.2-bookworm

ARG ZIG_VERSION=0.15.2
ARG ZIG_VERSION=0.16.0

ENV PATH=/opt/zig:$PATH

Expand Down
6 changes: 4 additions & 2 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Orchestrator. Spawns each (target, workload) combination as an isolated subproce
Usage: bench [options]
-t, --targets LIST Comma-separated target IDs (default: all)
-w, --workloads LIST Comma-separated workload IDs (default: all)
-r, --runs N Number of runs per combo, takes median (default: 1)
-r, --runs N Number of runs per combo, takes median (default: 3)
--timeout N Per-workload timeout in seconds (default: 30)
--setup Install target gems and exit
```
Expand All @@ -62,6 +62,8 @@ Output goes to stdout (the table). Progress and setup messages go to stderr.

When exactly 2 targets are specified, a delta column shows the percentage change from the first target to the second.

When `runs >= 2`, each cell is annotated with the coefficient of variation (e.g. `±2.5%`), and the delta column tags entries as `(noisy)` when the magnitude of the change is within either target's CV. Use this to tell genuine regressions/wins from run-to-run jitter.

### `benchmarks/bench_one`

Worker subprocess. Not meant to be called directly. Loads one target scheduler, runs one workload, prints a single floating-point metric value to stdout.
Expand Down Expand Up @@ -163,7 +165,7 @@ Orchestrator for async benchmarks. Same CLI interface as `bench`.
Usage: async_bench [options]
-t, --targets LIST Comma-separated target IDs: stock, carbon (default: all)
-w, --workloads LIST Comma-separated workload IDs (default: all)
-r, --runs N Number of runs per combo, takes median (default: 1)
-r, --runs N Number of runs per combo, takes median (default: 3)
--timeout N Per-workload timeout in seconds (default: 30)
```

Expand Down
Loading
Loading