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
10 changes: 7 additions & 3 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ concurrency:
jobs:
benchmark:
name: Run Benchmark Suite
runs-on: [self-hosted, linux, benchmark]
runs-on: [self-hosted, linux, x64, benchmark-dedicated]
env:
BENCHMARK_FRAMEWORK_FILTER: ${{ inputs.framework_filter || 'all' }}
BENCHMARK_DEDICATED_HOST: '1'
BENCHMARK_TARGET_CPUS: '2-7'
BENCHMARK_LOAD_CPUS: '8-15'
BENCHMARK_CGROUP_PARENT: framework-benchmark
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -41,13 +45,13 @@ jobs:
${{ runner.os }}-cargo-benchmark-

- name: Verify host environment
run: sh scripts/verify_host.sh
run: sh scripts/verify_host.sh --require-dedicated

- name: Build benchmark orchestrator
run: cargo build --release --bin orchestrator

- name: Run orchestrator
run: ./target/release/orchestrator frameworks results
run: taskset --cpu-list "$BENCHMARK_LOAD_CPUS" ./target/release/orchestrator frameworks results

- name: Upload benchmark artifacts
uses: actions/upload-artifact@v4
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/benchmark_smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Benchmark Smoke

on:
pull_request:
paths:
- frameworks/**
- templates/docker/**
- scripts/**
- flake.nix
- .github/workflows/benchmark_smoke.yml
push:
branches: [main]
paths:
- frameworks/**
- templates/docker/**
- scripts/**
- flake.nix
- .github/workflows/benchmark_smoke.yml
workflow_dispatch:

permissions:
contents: read

jobs:
smoke:
name: Dual-libc cpeak health smoke
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build and exercise both variants
shell: bash
working-directory: frameworks/web-server/rust/cpeak
run: |
set -euo pipefail
for libc in glibc musl; do
image="benchmark-smoke:cpeak-${libc}"
name="benchmark-smoke-${libc}"
docker build --file "Dockerfile.${libc}" --tag "$image" .
docker run --detach --rm --network host --name "$name" "$image"
for _ in $(seq 1 20); do
curl --fail --silent --show-error http://127.0.0.1:8080/healthz && break
sleep 1
done
test "$(curl --fail --silent http://127.0.0.1:8080/plaintext)" = 'Hello, World!'
docker stop --time 5 "$name"
done
72 changes: 72 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ hdrhistogram = "7.5"
bytes = "1.10"
http = "1.2"
httparse = "1.10"
sha2 = "0.10"

[profile.release]
opt-level = 3
Expand Down
8 changes: 4 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The Docker-based dual-libc baseline is implemented, but a result is publishable
3. **Phase 2: Dual-Libc Infrastructure & Engine Core**: [docs/roadmap/phase2_load_engine.md](docs/roadmap/phase2_load_engine.md) [Completed]
- Nix flakes, Docker multi-stage templates, Tokio/Httparse load generator, and cgroups v2 harvester.
4. **Phase 3: Framework Registry & Baseline Implementations**: [docs/roadmap/phase3_frameworks.md](docs/roadmap/phase3_frameworks.md) [Completed]
- Framework registry populated with 32 candidate frameworks across 10 languages (Zig, Rust, Go, C#, C++, JS/TS, Ruby, Java, PHP, Python) with dual-libc Dockerfiles and valid manifests.
- Framework registry populated with 31 candidate frameworks across 10 languages (Zig, Rust, Go, C#, C++, JS/TS, Ruby, Java, PHP, Python) with dual-libc Dockerfiles and valid manifests.
5. **Phase 4: Aggregation, Metrics Processing & Markdown Engine**: [docs/roadmap/phase4_reporter.md](docs/roadmap/phase4_reporter.md) [Completed]
- Pure aggregation logic, SVG bar charts, append-only historical index, and Markdown table generator.
6. **Phase 5: Bare-Metal Deployment & GitHub CI/CD**: [docs/roadmap/phase5_deployment.md](docs/roadmap/phase5_deployment.md) [Completed]
Expand All @@ -43,9 +43,9 @@ The Docker-based dual-libc baseline is implemented, but a result is publishable

## Next milestones

- Host-managed cgroups and CPU sets for complete collector attribution.
- Full-matrix execution on a labelled self-hosted runner; deterministic smoke validation in hosted CI.
- Immutable image and framework input pinning before historical comparison.
- Host-managed cgroups and CPU sets for complete collector attribution. [Completed]
- Full-matrix execution on a labelled self-hosted runner. [Requires designated host]
- Immutable image and framework input pinning before historical comparison. [Completed]

See [docs/how_to_use.md](docs/how_to_use.md) for the supported host workflow.

Expand Down
123 changes: 115 additions & 8 deletions crates/collector/src/cgroups.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::error::CollectorError;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct CgroupSample {
Expand All @@ -8,22 +11,126 @@ pub struct CgroupSample {
pub system_usec: u64,
}

#[derive(Debug)]
pub struct CgroupFiles {
memory_current: Option<File>,
memory_peak: Option<File>,
cpu_stat: Option<File>,
}

pub fn parse_cgroup_memory_stat(content: &str) -> Result<u64, CollectorError> {
content
.trim()
.parse::<u64>()
.map_err(|e| CollectorError::Parse(format!("invalid cgroup memory stat: {e}")))
.map_err(|error| CollectorError::Parse(format!("invalid cgroup memory stat: {error}")))
}

pub fn parse_cgroup_cpu_stat(content: &str) -> (u64, u64) {
let mut user = 0;
let mut system = 0;
let mut user_usec = 0;
let mut system_usec = 0;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("user_usec ") {
user = rest.trim().parse().unwrap_or(0);
} else if let Some(rest) = line.strip_prefix("system_usec ") {
system = rest.trim().parse().unwrap_or(0);
if let Some(value) = line.strip_prefix("user_usec ") {
user_usec = value.trim().parse().unwrap_or(0);
} else if let Some(value) = line.strip_prefix("system_usec ") {
system_usec = value.trim().parse().unwrap_or(0);
}
}
(user_usec, system_usec)
}

pub fn open_cgroup_files(path: &Path) -> CgroupFiles {
CgroupFiles {
memory_current: File::open(path.join("memory.current")).ok(),
memory_peak: File::open(path.join("memory.peak")).ok(),
cpu_stat: File::open(path.join("cpu.stat")).ok(),
}
}

pub fn sample_cgroup_files(files: &mut CgroupFiles) -> CgroupSample {
let current_bytes = read_number(&mut files.memory_current);
let peak_bytes = read_number(&mut files.memory_peak);
let (user_usec, system_usec) = read_cpu_stat(&mut files.cpu_stat);
CgroupSample {
current_bytes,
peak_bytes,
user_usec,
system_usec,
}
}

fn read_number(file: &mut Option<File>) -> u64 {
let Some(file) = file.as_mut() else {
return 0;
};
let mut buffer = [0; 32];
if file.seek(SeekFrom::Start(0)).is_err() {
return 0;
}
let Ok(count) = file.read(&mut buffer) else {
return 0;
};
parse_number_bytes(&buffer[..count])
}

fn read_cpu_stat(file: &mut Option<File>) -> (u64, u64) {
let Some(file) = file.as_mut() else {
return (0, 0);
};
let mut buffer = [0; 256];
if file.seek(SeekFrom::Start(0)).is_err() {
return (0, 0);
}
let Ok(count) = file.read(&mut buffer) else {
return (0, 0);
};
let Ok(content) = std::str::from_utf8(&buffer[..count]) else {
return (0, 0);
};
parse_cgroup_cpu_stat(content)
}

fn parse_number_bytes(content: &[u8]) -> u64 {
let mut value = 0_u64;
for byte in content {
if !byte.is_ascii_digit() {
break;
}
value = value
.saturating_mul(10)
.saturating_add(u64::from(byte - b'0'));
}
value
}

#[cfg(test)]
mod tests {
use super::{CgroupSample, open_cgroup_files, sample_cgroup_files};
use std::fs;

#[test]
fn cgroup_files_should_sample_all_controllers() {
let path = std::env::temp_dir().join(format!("collector-cgroups-{}", std::process::id()));
fs::create_dir_all(&path).expect("create cgroup fixture directory");
fs::write(path.join("memory.current"), "4096\n").expect("write current memory fixture");
fs::write(path.join("memory.peak"), "8192\n").expect("write peak memory fixture");
fs::write(
path.join("cpu.stat"),
"usage_usec 10\nuser_usec 7\nsystem_usec 3\n",
)
.expect("write cpu fixture");

let mut files = open_cgroup_files(&path);
let sample = sample_cgroup_files(&mut files);
fs::remove_dir_all(&path).expect("remove cgroup fixture directory");

assert_eq!(
sample,
CgroupSample {
current_bytes: 4096,
peak_bytes: 8192,
user_usec: 7,
system_usec: 3,
}
);
}
(user, system)
}
Loading
Loading