#!/usr/bin/env bash set -euo pipefail # Local GET benchmark harness for the experimental codec streaming read path. # # This script intentionally keeps the benchmark orchestration thin: # - start one local RustFS server with controlled GET/codec/scanner env # - run the existing enhanced object benchmark in GET mode # - optionally run legacy and codec profiles back-to-back for A/B comparison SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" ENHANCED_BENCH="${PROJECT_ROOT}/scripts/run_object_batch_bench_enhanced.sh" ADDRESS="127.0.0.1:19030" ACCESS_KEY="rustfsadmin" SECRET_KEY="rustfsadmin" BUCKET="rustfs-get-codec-smoke" REGION="us-east-1" SIZES="1MiB,4MiB,10MiB" CONCURRENCY=16 DURATION="30s" ROUNDS=3 RETRY_PER_ROUND=1 ROUND_COOLDOWN_SECS=20 WARP_OBJECTS="" WARP_OBJECT_LIFECYCLE="per-round" WARP_PREPARE_DURATION="1s" WARP_MODE="get" WARP_EXTRA_ARGS="" WARP_WARMUP_GET_BEFORE_BENCH=false GET_OBJECT_METADATA_CACHE_MAX_ENTRIES="" GET_SMALL_OBJECT_DIRECT_MEMORY="" GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD="" LOCAL_READ_COPY_METHOD="" MODE="both" PROFILE_ORDER="normal" CODEC_ENGINES="legacy" CODEC_MAX_INFLIGHT=1 CODEC_READER_SETUP="all_shards" CODEC_DATA_BLOCKS_FIRST_GATE="off" CODEC_DATA_BLOCKS_FIRST_MAX_SIZE="" CODEC_MULTIPART="off" CODEC_MULTIPART_MAX_PARTS=256 METADATA_EARLY_STOP="off" SHARD_LOCALITY_PREFERENCE="off" OUTPUT_HANDOFF_ATTRIBUTION=false DIAGNOSTIC_METRICS=false DIAGNOSTIC_METRICS_URL="http://127.0.0.1:8889/metrics" DIAGNOSTIC_PROMETHEUS_QUERY_URL="" DIAGNOSTIC_PROMETHEUS_QUERY='{__name__=~"rustfs_io_get_object_.*"}' DIAGNOSTIC_METRICS_SETTLE_SECS="${RUSTFS_DIAGNOSTIC_METRICS_SETTLE_SECS:-2}" DIAGNOSTIC_OBS_ENDPOINT="${RUSTFS_OBS_ENDPOINT:-}" DIAGNOSTIC_OBS_METRIC_ENDPOINT="${RUSTFS_OBS_METRIC_ENDPOINT:-}" DIAGNOSTIC_OBS_METER_INTERVAL="${RUSTFS_OBS_METER_INTERVAL:-1}" DIAGNOSTIC_OBS_SERVICE_NAME_PREFIX="${RUSTFS_OBS_SERVICE_NAME:-RustFS-get-codec}" DIAGNOSTIC_METRICS_CAPTURE_ATTEMPTS="${RUSTFS_DIAGNOSTIC_METRICS_CAPTURE_ATTEMPTS:-5}" DIAGNOSTIC_METRICS_CAPTURE_RETRY_SECS="${RUSTFS_DIAGNOSTIC_METRICS_CAPTURE_RETRY_SECS:-1}" DIAGNOSTIC_METRICS_CONNECT_TIMEOUT_SECS="${RUSTFS_DIAGNOSTIC_METRICS_CONNECT_TIMEOUT_SECS:-2}" DIAGNOSTIC_METRICS_MAX_TIME_SECS="${RUSTFS_DIAGNOSTIC_METRICS_MAX_TIME_SECS:-15}" DIAGNOSTIC_METRICS_FILTER_REGEX="${RUSTFS_DIAGNOSTIC_METRICS_FILTER_REGEX:-rustfs_io_get_object_}" SERVICE_METRIC_PREFIX="rustfs_io_get_object_" COMPRESSED_FALLBACK_PROBE=false COMPRESSED_PROBE_EXTENSION=".compressed-probe.txt" COMPRESSED_PROBE_MIME_TYPE="text/plain" OUT_DIR="" RUSTFS_BIN="${PROJECT_ROOT}/target/release/rustfs" WARP_BIN="warp" PYTHON_BIN="python3" CODEC_MIN_SIZE="" DEFAULT_CODEC_MIN_SIZE=1048576 DEFAULT_RUSTFS_CODEC_MIN_SIZE=1048576 RUST_LOG="warn" HEALTH_TIMEOUT_SECS=60 COMPAT_OBJECT_KEY="__rustfs_get_v2_pr24_compat/object.bin" COMPAT_OBJECT_SIZE=65536 SKIP_COMPAT_PROBE=false RESOURCE_SAMPLE_INTERVAL_SECS="${RUSTFS_GET_BENCH_RESOURCE_SAMPLE_INTERVAL_SECS:-5}" DRY_RUN=false SKIP_BUILD=false DETACH=false DETACH_LOG="" PROFILE_FAILURES=0 declare -a ORIGINAL_ARGS=() SERVER_PID="" SERVER_SAMPLER_PID="" SERVER_SAMPLER_LOG="" usage() { cat <<'USAGE' Usage: scripts/run_get_codec_streaming_smoke.sh [options] Purpose: Start a local RustFS server and run GET warp benchmarks for the legacy read path, the experimental codec streaming read path, or both. Core options: --mode Which profile(s) to run (default: both) --profile-order Profile execution order after --mode expansion (default: normal) --codec-engine Codec engine(s) for codec profiles (default: legacy) --metadata-early-stop Enable metadata early-stop observe/opt-in env (default: off) --shard-locality-preference Enable shard locality preference env (default: off) --codec-max-inflight RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT (default: 1) --codec-reader-setup RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_READER_SETUP for codec verify reader setup (default: all_shards) --codec-data-blocks-first-gate Enable size-gated data-blocks-first reader setup for codec-rustfs plain single-part GETs (default: off) --codec-data-blocks-first-max-size RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_MAX_SIZE for the gated path (default: server default) --codec-multipart Enable multipart codec streaming opt-in for codec profiles (default: off) --codec-multipart-max-parts RUSTFS_GET_CODEC_STREAMING_MULTIPART_MAX_PARTS (default: 256) --handoff-attribution Enable output handoff attribution metrics --diagnostic-metrics Enable observability metrics export and capture server-side metrics --diagnostic-metrics-url Prometheus scrape URL for diagnostic captures (default: http://127.0.0.1:8889/metrics) --diagnostic-prometheus-query-url Prometheus HTTP API query endpoint for OTLP-exported metrics, e.g. http://localhost:9090/api/v1/query --diagnostic-prometheus-query PromQL used with --diagnostic-prometheus-query-url (default: {__name__=~"rustfs_io_get_object_.*"}) --diagnostic-metrics-settle-secs Seconds to wait before the after snapshot so OTLP periodic metrics can export probe counters (default: 2) --diagnostic-metrics-capture-attempts Retry attempts for each direct /metrics scrape (default: 5) --diagnostic-metrics-capture-retry-secs Sleep seconds between failed direct /metrics scrape attempts (default: 1) --diagnostic-metrics-connect-timeout-secs Curl connect timeout for each direct /metrics scrape (default: 2) --diagnostic-metrics-max-time-secs Curl max time for each direct /metrics scrape (default: 15) --diagnostic-metrics-filter-regex Regex for retained direct /metrics lines (default: rustfs_io_get_object_) --diagnostic-obs-endpoint RUSTFS_OBS_ENDPOINT passed to RustFS during diagnostic runs --diagnostic-obs-metric-endpoint RUSTFS_OBS_METRIC_ENDPOINT passed to RustFS during diagnostic runs --diagnostic-obs-meter-interval RUSTFS_OBS_METER_INTERVAL passed during diagnostic runs (default: 1) --diagnostic-obs-service-name-prefix Prefix used for unique per-profile RUSTFS_OBS_SERVICE_NAME (default: RustFS-get-codec, or existing RUSTFS_OBS_SERVICE_NAME) --compressed-fallback-probe Enable disk compression only for the compressed fallback probe object --address RustFS listen address (default: 127.0.0.1:19030) --bucket Benchmark bucket (default: rustfs-get-codec-smoke) --sizes Object sizes (default: 1MiB,4MiB,10MiB) --concurrency warp concurrency (default: 16) --duration warp duration per round (default: 30s) --warp-objects Number of objects prepared by warp for each size (default: warp default) --warp-mode warp workload mode used by the benchmark (default: get) --warp-extra-args Extra arguments passed to the warp benchmark command after lifecycle arguments --warp-warmup-get-before-bench Run a GET warmup with --noclear before the measured benchmark. Useful for read/write invalidation workloads. --warp-object-lifecycle Object lifecycle mode for warp GET: per-round|prepare-once|existing-only (default: per-round) --warp-prepare-duration Duration used by the prepare-once warmup warp GET run (default: 1s) --metadata-cache-max-entries RUSTFS_GET_OBJECT_METADATA_CACHE_MAX_ENTRIES for RustFS --direct-memory RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY for RustFS --direct-memory-threshold RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD for RustFS --local-read-copy-method RUSTFS_OBJECT_MMAP_READ_METHOD for RustFS --rounds rounds per size (default: 3) --retry-per-round failed-attempt retries per round (default: 1) --round-cooldown-secs cooldown seconds after each completed round (default: 20) --out-dir output directory (default: target/bench/get-codec-streaming-) Binary/options: --rustfs-bin RustFS binary (default: target/release/rustfs) --warp-bin warp binary (default: warp) --python-bin Python binary for SigV4 compatibility probe (default: python3) --codec-min-size Override RUSTFS_GET_CODEC_STREAMING_MIN_SIZE (default: unset, use server size-aware default) --compat-object-key Object key used by the compatibility probe --compat-object-size Object size used by the compatibility probe (default: 65536) --skip-compat-probe skip post-benchmark compatibility/fallback probes --resource-sample-interval-secs Process resource sampler interval (default: 5) --skip-build do not run cargo build --release -p rustfs --detach start this script under nohup and exit after writing detached pid/log metadata --detach-log log file used with --detach (default: /detached.log) --dry-run print benchmark commands without starting RustFS Credentials: --access-key RustFS access key (default: rustfsadmin) --secret-key RustFS secret key (default: rustfsadmin) --region S3 region (default: us-east-1) Output: /environment.txt /manifest.env /baseline_compare.csv /cpu_rss_notes.txt /fallback_coverage.txt /tracking_issues.txt /raw_output_paths.txt /default_switch_readiness.md /legacy/warp/median_summary.csv /codec-legacy/warp/median_summary.csv /codec-rustfs/warp/median_summary.csv /engine_compare.csv when legacy and codec profiles both run /compat_summary.csv when legacy and codec profiles both run /metrics_summary.csv /service_metrics_summary.csv when --diagnostic-metrics is set /service_metrics_round_summary.csv per-round stage/page-fault deltas when direct --diagnostic-metrics is set /service_metrics_stage_distribution.csv per-round stage histogram bucket deltas when direct --diagnostic-metrics is set /service_metrics_round_percentiles.csv per-round request/stage histogram percentile bucket upper bounds /service_metrics_acceptance.csv when --diagnostic-metrics is set /fallback_probe_summary.csv /body_sha256_legacy.txt when legacy profile runs /body_sha256_codec_legacy.txt when codec-legacy profile runs /body_sha256_codec_rustfs.txt when codec-rustfs profile runs /response_headers_legacy.json when legacy profile runs /response_headers_codec_legacy.json when codec-legacy profile runs /response_headers_codec_rustfs.json when codec-rustfs profile runs //manifest.env //metrics_summary.csv //service_metrics_summary.csv //service_metrics_round_summary.csv //service_metrics_stage_distribution.csv //service_metrics_round_percentiles.csv //service-metrics/*.prom before/after snapshots when --diagnostic-metrics is set //service-metrics/rounds/*.prom per-round snapshots when direct --diagnostic-metrics is set //compat/compat_summary.csv //compat/fallback_probe_summary.csv //compat/response_headers.json //compat/body_sha256.txt //rustfs.log //warp/logs/*.log Example: scripts/run_get_codec_streaming_smoke.sh \ --mode both --codec-engine legacy,rustfs --sizes 1MiB,4MiB,10MiB --concurrency 64 --duration 30s USAGE } log() { printf '%s\n' "$*" } die() { echo "ERROR: $*" >&2 exit 1 } require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then die "command not found: $1" fi } validate_positive_int() { local value="$1" local name="$2" if ! [[ "$value" =~ ^[0-9]+$ ]] || [[ "$value" -le 0 ]]; then die "$name must be a positive integer, got: $value" fi } validate_non_negative_int() { local value="$1" local name="$2" if ! [[ "$value" =~ ^[0-9]+$ ]]; then die "$name must be a non-negative integer, got: $value" fi } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="$2"; shift 2 ;; --profile-order) PROFILE_ORDER="$2"; shift 2 ;; --codec-engine) CODEC_ENGINES="$2"; shift 2 ;; --metadata-early-stop) METADATA_EARLY_STOP="$2"; shift 2 ;; --shard-locality-preference) SHARD_LOCALITY_PREFERENCE="$2"; shift 2 ;; --codec-max-inflight) CODEC_MAX_INFLIGHT="$2"; shift 2 ;; --codec-reader-setup) CODEC_READER_SETUP="$2"; shift 2 ;; --codec-data-blocks-first-gate) CODEC_DATA_BLOCKS_FIRST_GATE="$2"; shift 2 ;; --codec-data-blocks-first-max-size) CODEC_DATA_BLOCKS_FIRST_MAX_SIZE="$2"; shift 2 ;; --codec-multipart) CODEC_MULTIPART="$2"; shift 2 ;; --codec-multipart-max-parts) CODEC_MULTIPART_MAX_PARTS="$2"; shift 2 ;; --handoff-attribution) OUTPUT_HANDOFF_ATTRIBUTION=true; shift ;; --diagnostic-metrics) DIAGNOSTIC_METRICS=true; shift ;; --diagnostic-metrics-url) DIAGNOSTIC_METRICS_URL="$2"; shift 2 ;; --diagnostic-prometheus-query-url) DIAGNOSTIC_PROMETHEUS_QUERY_URL="$2"; shift 2 ;; --diagnostic-prometheus-query) DIAGNOSTIC_PROMETHEUS_QUERY="$2"; shift 2 ;; --diagnostic-metrics-settle-secs) DIAGNOSTIC_METRICS_SETTLE_SECS="$2"; shift 2 ;; --diagnostic-metrics-capture-attempts) DIAGNOSTIC_METRICS_CAPTURE_ATTEMPTS="$2"; shift 2 ;; --diagnostic-metrics-capture-retry-secs) DIAGNOSTIC_METRICS_CAPTURE_RETRY_SECS="$2"; shift 2 ;; --diagnostic-metrics-connect-timeout-secs) DIAGNOSTIC_METRICS_CONNECT_TIMEOUT_SECS="$2"; shift 2 ;; --diagnostic-metrics-max-time-secs) DIAGNOSTIC_METRICS_MAX_TIME_SECS="$2"; shift 2 ;; --diagnostic-metrics-filter-regex) DIAGNOSTIC_METRICS_FILTER_REGEX="$2"; shift 2 ;; --diagnostic-obs-endpoint) DIAGNOSTIC_OBS_ENDPOINT="$2"; shift 2 ;; --diagnostic-obs-metric-endpoint) DIAGNOSTIC_OBS_METRIC_ENDPOINT="$2"; shift 2 ;; --diagnostic-obs-meter-interval) DIAGNOSTIC_OBS_METER_INTERVAL="$2"; shift 2 ;; --diagnostic-obs-service-name-prefix) DIAGNOSTIC_OBS_SERVICE_NAME_PREFIX="$2"; shift 2 ;; --compressed-fallback-probe) COMPRESSED_FALLBACK_PROBE=true; shift ;; --address) ADDRESS="$2"; shift 2 ;; --bucket) BUCKET="$2"; shift 2 ;; --sizes) SIZES="$2"; shift 2 ;; --concurrency) CONCURRENCY="$2"; shift 2 ;; --duration) DURATION="$2"; shift 2 ;; --warp-objects) WARP_OBJECTS="$2"; shift 2 ;; --warp-mode) WARP_MODE="$2"; shift 2 ;; --warp-extra-args) WARP_EXTRA_ARGS="$2"; shift 2 ;; --warp-warmup-get-before-bench) WARP_WARMUP_GET_BEFORE_BENCH=true; shift ;; --warp-object-lifecycle) WARP_OBJECT_LIFECYCLE="$2"; shift 2 ;; --warp-prepare-duration) WARP_PREPARE_DURATION="$2"; shift 2 ;; --metadata-cache-max-entries) GET_OBJECT_METADATA_CACHE_MAX_ENTRIES="$2"; shift 2 ;; --direct-memory) GET_SMALL_OBJECT_DIRECT_MEMORY="$2"; shift 2 ;; --direct-memory-threshold) GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD="$2"; shift 2 ;; --local-read-copy-method) LOCAL_READ_COPY_METHOD="$2"; shift 2 ;; --rounds) ROUNDS="$2"; shift 2 ;; --retry-per-round) RETRY_PER_ROUND="$2"; shift 2 ;; --round-cooldown-secs) ROUND_COOLDOWN_SECS="$2"; shift 2 ;; --out-dir) OUT_DIR="$2"; shift 2 ;; --rustfs-bin) RUSTFS_BIN="$2"; shift 2 ;; --warp-bin) WARP_BIN="$2"; shift 2 ;; --python-bin) PYTHON_BIN="$2"; shift 2 ;; --codec-min-size) CODEC_MIN_SIZE="$2"; shift 2 ;; --compat-object-key) COMPAT_OBJECT_KEY="$2"; shift 2 ;; --compat-object-size) COMPAT_OBJECT_SIZE="$2"; shift 2 ;; --skip-compat-probe) SKIP_COMPAT_PROBE=true; shift ;; --resource-sample-interval-secs) RESOURCE_SAMPLE_INTERVAL_SECS="$2"; shift 2 ;; --access-key) ACCESS_KEY="$2"; shift 2 ;; --secret-key) SECRET_KEY="$2"; shift 2 ;; --region) REGION="$2"; shift 2 ;; --skip-build) SKIP_BUILD=true; shift ;; --detach) DETACH=true; shift ;; --detach-log) DETACH_LOG="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; -h|--help) usage; exit 0 ;; *) usage >&2 die "unknown arg: $1" ;; esac done } validate_args() { case "$MODE" in legacy|codec|both) ;; *) die "--mode must be legacy, codec, or both" ;; esac case "$PROFILE_ORDER" in normal|reverse) ;; *) die "--profile-order must be normal or reverse" ;; esac case "$METADATA_EARLY_STOP" in on|off) ;; *) die "--metadata-early-stop must be on or off" ;; esac case "$SHARD_LOCALITY_PREFERENCE" in on|off) ;; *) die "--shard-locality-preference must be on or off" ;; esac case "$CODEC_MULTIPART" in on|off) ;; *) die "--codec-multipart must be on or off" ;; esac local raw engine IFS=',' read -r -a engines <<< "$CODEC_ENGINES" [[ "${#engines[@]}" -gt 0 ]] || die "--codec-engine must not be empty" for raw in "${engines[@]}"; do engine="${raw//[[:space:]]/}" case "$engine" in legacy|rustfs) ;; *) die "--codec-engine supports only legacy,rustfs, got: $engine" ;; esac done [[ -n "$ADDRESS" ]] || die "--address must not be empty" [[ -n "$ACCESS_KEY" ]] || die "--access-key must not be empty" [[ -n "$SECRET_KEY" ]] || die "--secret-key must not be empty" [[ -n "$BUCKET" ]] || die "--bucket must not be empty" [[ -n "$SIZES" ]] || die "--sizes must not be empty" validate_positive_int "$CONCURRENCY" "--concurrency" validate_positive_int "$CODEC_MAX_INFLIGHT" "--codec-max-inflight" case "$CODEC_READER_SETUP" in all_shards|data_blocks_first) ;; *) die "--codec-reader-setup must be all_shards or data_blocks_first" ;; esac case "$CODEC_DATA_BLOCKS_FIRST_GATE" in on|off) ;; *) die "--codec-data-blocks-first-gate must be on or off" ;; esac if [[ -n "$CODEC_DATA_BLOCKS_FIRST_MAX_SIZE" ]]; then validate_positive_int "$CODEC_DATA_BLOCKS_FIRST_MAX_SIZE" "--codec-data-blocks-first-max-size" fi validate_positive_int "$CODEC_MULTIPART_MAX_PARTS" "--codec-multipart-max-parts" validate_positive_int "$ROUNDS" "--rounds" validate_positive_int "$RETRY_PER_ROUND" "--retry-per-round" validate_non_negative_int "$ROUND_COOLDOWN_SECS" "--round-cooldown-secs" if [[ -n "$WARP_OBJECTS" ]]; then validate_positive_int "$WARP_OBJECTS" "--warp-objects" fi case "$WARP_MODE" in get|mixed) ;; *) die "--warp-mode must be get or mixed" ;; esac case "$WARP_OBJECT_LIFECYCLE" in per-round|prepare-once|existing-only) ;; *) die "--warp-object-lifecycle must be per-round, prepare-once, or existing-only" ;; esac if [[ -n "$GET_OBJECT_METADATA_CACHE_MAX_ENTRIES" ]]; then validate_positive_int "$GET_OBJECT_METADATA_CACHE_MAX_ENTRIES" "--metadata-cache-max-entries" fi if [[ -n "$GET_SMALL_OBJECT_DIRECT_MEMORY" ]]; then case "$GET_SMALL_OBJECT_DIRECT_MEMORY" in on|off) ;; *) die "--direct-memory must be on or off" ;; esac fi if [[ -n "$GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD" ]]; then validate_positive_int "$GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD" "--direct-memory-threshold" fi if [[ -n "$LOCAL_READ_COPY_METHOD" ]]; then case "$LOCAL_READ_COPY_METHOD" in mmap_copy|direct_read_copy) ;; *) die "--local-read-copy-method must be mmap_copy or direct_read_copy" ;; esac fi if [[ -n "$CODEC_MIN_SIZE" ]]; then validate_positive_int "$CODEC_MIN_SIZE" "--codec-min-size" fi validate_positive_int "$COMPAT_OBJECT_SIZE" "--compat-object-size" validate_positive_int "$RESOURCE_SAMPLE_INTERVAL_SECS" "--resource-sample-interval-secs" validate_positive_int "$HEALTH_TIMEOUT_SECS" "--health-timeout-secs" validate_non_negative_int "$DIAGNOSTIC_METRICS_SETTLE_SECS" "--diagnostic-metrics-settle-secs" validate_positive_int "$DIAGNOSTIC_METRICS_CAPTURE_ATTEMPTS" "--diagnostic-metrics-capture-attempts" validate_non_negative_int "$DIAGNOSTIC_METRICS_CAPTURE_RETRY_SECS" "--diagnostic-metrics-capture-retry-secs" validate_positive_int "$DIAGNOSTIC_METRICS_CONNECT_TIMEOUT_SECS" "--diagnostic-metrics-connect-timeout-secs" validate_positive_int "$DIAGNOSTIC_METRICS_MAX_TIME_SECS" "--diagnostic-metrics-max-time-secs" validate_positive_int "$DIAGNOSTIC_OBS_METER_INTERVAL" "--diagnostic-obs-meter-interval" [[ -n "$COMPAT_OBJECT_KEY" ]] || die "--compat-object-key must not be empty" [[ -n "$DIAGNOSTIC_OBS_SERVICE_NAME_PREFIX" ]] || die "--diagnostic-obs-service-name-prefix must not be empty" [[ -n "$DIAGNOSTIC_METRICS_URL" ]] || die "--diagnostic-metrics-url must not be empty" [[ -x "$ENHANCED_BENCH" ]] || die "enhanced benchmark script is not executable: $ENHANCED_BENCH" require_cmd curl require_cmd git if [[ "$DRY_RUN" != "true" ]]; then require_cmd cargo require_cmd "$PYTHON_BIN" fi if [[ "$DETACH" == "true" && "$DRY_RUN" != "true" ]]; then require_cmd nohup fi } setup_output() { if [[ -z "$OUT_DIR" ]]; then OUT_DIR="${PROJECT_ROOT}/target/bench/get-codec-streaming-$(date +%Y%m%d-%H%M%S)" fi mkdir -p "$OUT_DIR" } bool_from_on_off() { case "$1" in on) echo "true" ;; off) echo "false" ;; *) die "expected on/off value, got: $1" ;; esac } codec_reader_setup_data_blocks_first() { case "$CODEC_READER_SETUP" in all_shards) echo "false" ;; data_blocks_first) echo "true" ;; *) die "expected codec reader setup strategy, got: $CODEC_READER_SETUP" ;; esac } dry_run_multipart_expected_reason() { if [[ "$CODEC_MULTIPART" != "on" ]]; then echo "multipart" elif [[ "$CODEC_MULTIPART_MAX_PARTS" -lt 2 ]]; then echo "multipart_part_limit" else echo "none" fi } command_line_string() { printf '%q ' "${BASH_SOURCE[0]}" "${ORIGINAL_ARGS[@]}" } detached_child_args() { local skip_next=false local arg for arg in "${ORIGINAL_ARGS[@]}"; do if [[ "$skip_next" == "true" ]]; then skip_next=false continue fi case "$arg" in --detach) ;; --detach-log) skip_next=true ;; *) printf '%s\0' "$arg" ;; esac done } run_detached_if_requested() { if [[ "$DETACH" != "true" ]]; then return 0 fi if [[ -z "$DETACH_LOG" ]]; then DETACH_LOG="${OUT_DIR}/detached.log" fi local script_path="${PROJECT_ROOT}/scripts/run_get_codec_streaming_smoke.sh" local pid_file="${OUT_DIR}/detached.pid" local command_file="${OUT_DIR}/detached_command.txt" local -a child_args=() while IFS= read -r -d '' arg; do child_args+=("$arg") done < <(detached_child_args) { printf '%q ' "$script_path" "${child_args[@]}" printf '\n' } >"$command_file" if [[ "$DRY_RUN" == "true" ]]; then log "[DRY-RUN] detach requested" log "[DRY-RUN] detached log: ${DETACH_LOG}" log "[DRY-RUN] detached command: ${command_file}" return 0 fi nohup "$script_path" "${child_args[@]}" >"$DETACH_LOG" 2>&1 < /dev/null & local detached_pid="$!" printf '%s\n' "$detached_pid" >"$pid_file" log "Detached GET codec streaming smoke started." log "PID: ${detached_pid}" log "Output dir: ${OUT_DIR}" log "Log: ${DETACH_LOG}" log "Command: ${command_file}" exit 0 } sanitize_metric_label_value() { printf '%s' "$1" | tr -c '[:alnum:]_-' '-' } diagnostic_service_name() { local profile="$1" local run_id run_id="$(sanitize_metric_label_value "$(basename "$OUT_DIR")")" echo "${DIAGNOSTIC_OBS_SERVICE_NAME_PREFIX}-${run_id}-${profile}" } detect_cpu_summary() { if command -v sysctl >/dev/null 2>&1; then local brand logical brand="$(sysctl -n machdep.cpu.brand_string 2>/dev/null || true)" logical="$(sysctl -n hw.logicalcpu 2>/dev/null || true)" if [[ -n "$brand" && -n "$logical" ]]; then printf '%s (%s logical cores)\n' "$brand" "$logical" return fi fi if command -v lscpu >/dev/null 2>&1; then lscpu 2>/dev/null | awk -F: ' /^Model name:/ { model=$2 } /^CPU\(s\):/ { cpus=$2 } END { gsub(/^[ \t]+|[ \t]+$/, "", model) gsub(/^[ \t]+|[ \t]+$/, "", cpus) if (model != "" && cpus != "") { printf "%s (%s logical cores)\n", model, cpus } else if (model != "") { printf "%s\n", model } } ' return fi echo "unknown" } write_root_environment() { local branch git_head git_dirty_count rustc_version cargo_version command_line branch="$(git -C "$PROJECT_ROOT" rev-parse --abbrev-ref HEAD)" git_head="$(git -C "$PROJECT_ROOT" rev-parse HEAD)" git_dirty_count="$(git -C "$PROJECT_ROOT" status --porcelain | awk 'END { print NR + 0 }')" rustc_version="$(rustc --version 2>/dev/null || echo unavailable)" cargo_version="$(cargo --version 2>/dev/null || echo unavailable)" command_line="$(command_line_string)" cat >"${OUT_DIR}/environment.txt" </dev/null || uname -a) cpu=$(detect_cpu_summary) command_line=${command_line% } EOF } write_tracking_issues() { cat >"${OUT_DIR}/tracking_issues.txt" <<'EOF' rustfs/backlog#715 rustfs/backlog#716 rustfs/backlog#717 rustfs/backlog#718 rustfs/backlog#719 rustfs/backlog#720 rustfs/backlog#721 rustfs/backlog#722 rustfs/backlog#723 rustfs/backlog#724 rustfs/backlog#725 rustfs/backlog#726 rustfs/backlog#727 rustfs/backlog#758 rustfs/backlog#759 rustfs/backlog#760 rustfs/backlog#761 rustfs/backlog#762 rustfs/backlog#763 rustfs/backlog#764 EOF } write_fallback_coverage() { cat >"${OUT_DIR}/fallback_coverage.txt" <<'EOF' codec_streaming rollout fallback coverage proof ============================================ Static gate reasons: - disabled - rollout_not_opted_in - rollout_pct_not_selected - body_compatibility_unconfirmed - header_compatibility_unconfirmed - lock_optimization_disabled - range - below_min_size - encrypted - compressed - remote - multipart - multipart_part_limit - invalid_min_size - read_quorum_not_safe Relevant focused proof points: - set_disk::read::tests::codec_streaming_reader_gate_defaults_to_disabled - set_disk::read::tests::codec_streaming_engine_env_is_ignored_when_streaming_is_disabled - set_disk::read::tests::codec_streaming_reader_gate_requires_explicit_rollout_and_compat_confirmation - set_disk::read::tests::codec_streaming_reader_gate_is_conservative - set_disk::read::tests::codec_streaming_reader_build_falls_back_when_read_quorum_is_not_safe - erasure::codec::bridge::tests::rustfs_codec_decode_engine_rejects_inconsistent_reconstruction_sources - erasure::codec::bridge::tests::rustfs_codec_decode_engine_rejects_stale_data_source - erasure::coding::decode_reader::tests::erasure_decode_reader_rustfs_engine_recovers_after_bitrot_source_mismatch - rustfs_io_get_object_reconstruct_outcome_total{path,engine,outcome} Conclusion: - range / encrypted / compressed / remote objects remain on legacy fallback paths - multipart remains a legacy fallback by default and can be separately enabled with RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE=true - multipart codec streaming is bounded by RUSTFS_GET_CODEC_STREAMING_MULTIPART_MAX_PARTS - degraded reader setup remains opt-out via read_quorum_not_safe - codec-rustfs remains explicit opt-in through RUSTFS_GET_CODEC_STREAMING_ENGINE=rustfs - healthy codec-rustfs reads should report skip_data_complete instead of rustfs_called - rustfs_called is reserved for explicit reconstruction test/probe coverage and must not be required for healthy-read rollout - env kill switch remains available through RUSTFS_GET_CODEC_STREAMING_ENABLE=false EOF } write_root_manifest() { local profiles_csv="$1" local branch git_head git_dirty_count command_line branch="$(git -C "$PROJECT_ROOT" rev-parse --abbrev-ref HEAD)" git_head="$(git -C "$PROJECT_ROOT" rev-parse HEAD)" git_dirty_count="$(git -C "$PROJECT_ROOT" status --porcelain | awk 'END { print NR + 0 }')" command_line="$(command_line_string)" cat >"${OUT_DIR}/manifest.env" <"${profile_dir}/manifest.env" </dev/null 2>&1; then kill "$SERVER_SAMPLER_PID" >/dev/null 2>&1 || true wait "$SERVER_SAMPLER_PID" >/dev/null 2>&1 || true fi SERVER_SAMPLER_PID="" SERVER_SAMPLER_LOG="" fi if [[ -n "$SERVER_PID" ]]; then if kill -0 "$SERVER_PID" >/dev/null 2>&1; then kill "$SERVER_PID" >/dev/null 2>&1 || true wait "$SERVER_PID" >/dev/null 2>&1 || true fi SERVER_PID="" fi } wait_for_health() { local profile="$1" local log_file="$2" local health_url health_url="$(endpoint_url)/health" for ((attempt = 1; attempt <= HEALTH_TIMEOUT_SECS; attempt++)); do if curl -fsS --noproxy '*' --connect-timeout 2 --max-time 3 "$health_url" >/dev/null 2>&1; then return fi if [[ -n "$SERVER_PID" ]] && ! kill -0 "$SERVER_PID" >/dev/null 2>&1; then tail -n 80 "$log_file" >&2 || true die "RustFS exited before health check passed for profile: $profile" fi sleep 1 done tail -n 80 "$log_file" >&2 || true die "RustFS health check timed out for profile: $profile" } start_server() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local data_root local volumes local codec_enabled local codec_engine local metrics_path local rollout_target local body_compat_confirmed local header_compat_confirmed local obs_service_name local rustfs_log data_root="$(profile_data_root "$profile")" volumes="$(profile_volumes "$data_root")" codec_enabled="$(profile_codec_enabled "$profile")" codec_engine="$(profile_codec_engine "$profile")" metrics_path="$(profile_metrics_path "$profile")" rollout_target="$(profile_rollout_target "$profile")" body_compat_confirmed="$(profile_body_compat_confirmed "$profile")" header_compat_confirmed="$(profile_header_compat_confirmed "$profile")" obs_service_name="$(diagnostic_service_name "$profile")" rustfs_log="${profile_dir}/rustfs.log" mkdir -p "${data_root}/disk1" "${data_root}/disk2" "${data_root}/disk3" "${data_root}/disk4" write_manifest "$profile" "$profile_dir" "$codec_enabled" "$volumes" "$codec_engine" "$metrics_path" if [[ "$DRY_RUN" == "true" ]]; then log "[DRY-RUN] start RustFS profile=${profile} endpoint=$(endpoint_url)" log "[DRY-RUN] RUSTFS_VOLUMES=${volumes}" return fi [[ -x "$RUSTFS_BIN" ]] || die "RustFS binary is not executable: $RUSTFS_BIN" ( export RUSTFS_ADDRESS="$ADDRESS" export RUSTFS_ACCESS_KEY="$ACCESS_KEY" export RUSTFS_SECRET_KEY="$SECRET_KEY" export RUSTFS_VOLUMES="$volumes" export RUSTFS_CONSOLE_ENABLE=false export RUSTFS_GET_CODEC_STREAMING_ENABLE="$codec_enabled" export RUSTFS_GET_CODEC_STREAMING_ENGINE="$codec_engine" export RUSTFS_GET_CODEC_STREAMING_ROLLOUT="$rollout_target" export RUSTFS_GET_CODEC_STREAMING_BODY_COMPAT_CONFIRMED="$body_compat_confirmed" export RUSTFS_GET_CODEC_STREAMING_HEADER_COMPAT_CONFIRMED="$header_compat_confirmed" RUSTFS_GET_METADATA_EARLY_STOP_ENABLE="$(bool_from_on_off "$METADATA_EARLY_STOP")" export RUSTFS_GET_METADATA_EARLY_STOP_ENABLE RUSTFS_GET_SHARD_LOCALITY_PREFERENCE_ENABLE="$(bool_from_on_off "$SHARD_LOCALITY_PREFERENCE")" export RUSTFS_GET_SHARD_LOCALITY_PREFERENCE_ENABLE export RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT="$CODEC_MAX_INFLIGHT" RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_READER_SETUP="$(codec_reader_setup_data_blocks_first)" export RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_READER_SETUP RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_ENABLE="$(bool_from_on_off "$CODEC_DATA_BLOCKS_FIRST_GATE")" export RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_ENABLE if [[ -n "$CODEC_DATA_BLOCKS_FIRST_MAX_SIZE" ]]; then export RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_MAX_SIZE="$CODEC_DATA_BLOCKS_FIRST_MAX_SIZE" else unset RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_MAX_SIZE fi RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE="$(bool_from_on_off "$CODEC_MULTIPART")" export RUSTFS_GET_CODEC_STREAMING_MULTIPART_ENABLE export RUSTFS_GET_CODEC_STREAMING_MULTIPART_MAX_PARTS="$CODEC_MULTIPART_MAX_PARTS" export RUSTFS_GET_OUTPUT_HANDOFF_ATTRIBUTION_ENABLE="$OUTPUT_HANDOFF_ATTRIBUTION" if [[ -n "$CODEC_MIN_SIZE" ]]; then export RUSTFS_GET_CODEC_STREAMING_MIN_SIZE="$CODEC_MIN_SIZE" else unset RUSTFS_GET_CODEC_STREAMING_MIN_SIZE fi if [[ -n "$GET_OBJECT_METADATA_CACHE_MAX_ENTRIES" ]]; then export RUSTFS_GET_OBJECT_METADATA_CACHE_MAX_ENTRIES="$GET_OBJECT_METADATA_CACHE_MAX_ENTRIES" fi if [[ -n "$GET_SMALL_OBJECT_DIRECT_MEMORY" ]]; then RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY="$(bool_from_on_off "$GET_SMALL_OBJECT_DIRECT_MEMORY")" export RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY fi if [[ -n "$GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD" ]]; then export RUSTFS_GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD="$GET_SMALL_OBJECT_DIRECT_MEMORY_THRESHOLD" fi if [[ -n "$LOCAL_READ_COPY_METHOD" ]]; then export RUSTFS_OBJECT_MMAP_READ_METHOD="$LOCAL_READ_COPY_METHOD" fi export RUSTFS_OBS_METRICS_EXPORT_ENABLED="$DIAGNOSTIC_METRICS" if [[ "$DIAGNOSTIC_METRICS" == "true" ]]; then export RUSTFS_OBS_METER_INTERVAL="$DIAGNOSTIC_OBS_METER_INTERVAL" export RUSTFS_OBS_SERVICE_NAME="$obs_service_name" if [[ -n "$DIAGNOSTIC_OBS_ENDPOINT" ]]; then export RUSTFS_OBS_ENDPOINT="$DIAGNOSTIC_OBS_ENDPOINT" fi if [[ -n "$DIAGNOSTIC_OBS_METRIC_ENDPOINT" ]]; then export RUSTFS_OBS_METRIC_ENDPOINT="$DIAGNOSTIC_OBS_METRIC_ENDPOINT" fi fi export RUSTFS_COMPRESSION_ENABLED="$COMPRESSED_FALLBACK_PROBE" export RUSTFS_COMPRESSION_EXTENSIONS="$COMPRESSED_PROBE_EXTENSION" export RUSTFS_COMPRESSION_MIME_TYPES="$COMPRESSED_PROBE_MIME_TYPE" export RUSTFS_REGION="$REGION" export RUSTFS_RPC_SECRET="rustfs-get-codec-smoke-rpc-secret" export RUSTFS_SCANNER_ENABLED=false export RUSTFS_SCANNER_START_DELAY_SECS=3600 export RUSTFS_SCANNER_CYCLE=3600 export RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true export RUST_LOG exec "$RUSTFS_BIN" ) >"$rustfs_log" 2>&1 & SERVER_PID="$!" start_server_sampler "$SERVER_PID" "$profile_dir" wait_for_health "$profile" "$rustfs_log" } prepare_warp_existing_objects() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local prepare_dir="${profile_dir}/warp_prepare" if [[ "$WARP_OBJECT_LIFECYCLE" != "prepare-once" ]]; then return 0 fi mkdir -p "$prepare_dir" local size bucket size_slug while IFS= read -r size; do [[ -n "$size" ]] || continue bucket="$(bucket_for_size "$size")" size_slug="$(sanitize_bucket_suffix "$size")" local cmd=( "$WARP_BIN" get --host "$ADDRESS" --access-key "$ACCESS_KEY" --secret-key "$SECRET_KEY" --bucket "$bucket" --obj.size "$size" --concurrent "$CONCURRENCY" --duration "$WARP_PREPARE_DURATION" --region "$REGION" --noclear ) if [[ -n "$WARP_OBJECTS" ]]; then cmd+=(--objects "$WARP_OBJECTS") fi { printf 'profile=%s\n' "$profile" printf 'mode=prepare-once\n' printf 'size=%s\n' "$size" printf 'duration=%s\n' "$WARP_PREPARE_DURATION" printf 'bucket=%s\n' "$bucket" printf 'command=' printf '%q ' "${cmd[@]}" printf '\n' } >"${prepare_dir}/manifest-${size_slug}.env" if [[ "$DRY_RUN" == "true" ]]; then log "[DRY-RUN] prepare existing warp objects profile=${profile} size=${size} bucket=${bucket}" printf '[DRY-RUN] ' >"${prepare_dir}/prepare-${size_slug}.log" printf '%q ' "${cmd[@]}" >>"${prepare_dir}/prepare-${size_slug}.log" printf '\n' >>"${prepare_dir}/prepare-${size_slug}.log" continue fi log "Preparing existing warp objects profile=${profile} size=${size} bucket=${bucket} lifecycle=${WARP_OBJECT_LIFECYCLE}..." "${cmd[@]}" >"${prepare_dir}/prepare-${size_slug}.log" 2>&1 done < <(benchmark_sizes) } warmup_warp_get_before_bench() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local warmup_dir="${profile_dir}/warp_warmup_get" if [[ "$WARP_WARMUP_GET_BEFORE_BENCH" != "true" ]]; then return 0 fi mkdir -p "$warmup_dir" local size bucket size_slug while IFS= read -r size; do [[ -n "$size" ]] || continue bucket="$(bucket_for_size "$size")" size_slug="$(sanitize_bucket_suffix "$size")" local cmd=( "$WARP_BIN" get --host "$ADDRESS" --access-key "$ACCESS_KEY" --secret-key "$SECRET_KEY" --bucket "$bucket" --obj.size "$size" --concurrent "$CONCURRENCY" --duration "$WARP_PREPARE_DURATION" --region "$REGION" --noclear ) if [[ -n "$WARP_OBJECTS" ]]; then cmd+=(--objects "$WARP_OBJECTS") fi { printf 'profile=%s\n' "$profile" printf 'mode=warmup-get-before-bench\n' printf 'size=%s\n' "$size" printf 'duration=%s\n' "$WARP_PREPARE_DURATION" printf 'bucket=%s\n' "$bucket" printf 'command=' printf '%q ' "${cmd[@]}" printf '\n' } >"${warmup_dir}/manifest-${size_slug}.env" if [[ "$DRY_RUN" == "true" ]]; then log "[DRY-RUN] warmup GET before benchmark profile=${profile} size=${size} bucket=${bucket}" printf '[DRY-RUN] ' >"${warmup_dir}/warmup-${size_slug}.log" printf '%q ' "${cmd[@]}" >>"${warmup_dir}/warmup-${size_slug}.log" printf '\n' >>"${warmup_dir}/warmup-${size_slug}.log" continue fi log "Running warmup GET before benchmark profile=${profile} size=${size} bucket=${bucket}..." "${cmd[@]}" >"${warmup_dir}/warmup-${size_slug}.log" 2>&1 done < <(benchmark_sizes) } ensure_benchmark_buckets_exist() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local bucket_file="${profile_dir}/benchmark_buckets.txt" local bucket : >"$bucket_file" while IFS= read -r size; do [[ -n "$size" ]] || continue bucket="$(bucket_for_size "$size")" if ! grep -Fxq "$bucket" "$bucket_file"; then printf '%s\n' "$bucket" >>"$bucket_file" fi done < <(benchmark_sizes) if [[ "$DRY_RUN" == "true" ]]; then log "[DRY-RUN] ensure benchmark buckets exist profile=${profile} buckets=${bucket_file}" return fi "$PYTHON_BIN" - "$(endpoint_url)" "$ACCESS_KEY" "$SECRET_KEY" "$REGION" "$bucket_file" <<'PY' import datetime as dt import hashlib import hmac import http.client import pathlib import sys import urllib.parse endpoint, access_key, secret_key, region, bucket_file = sys.argv[1:] parsed = urllib.parse.urlsplit(endpoint) def sha256_hex(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sign(key: bytes, value: str) -> bytes: return hmac.new(key, value.encode(), hashlib.sha256).digest() def signing_key(secret: str, date_stamp: str) -> bytes: key_date = sign(("AWS4" + secret).encode(), date_stamp) key_region = sign(key_date, region) key_service = sign(key_region, "s3") return sign(key_service, "aws4_request") def canonical_uri(path: str) -> str: return "/".join(urllib.parse.quote(part, safe="-_.~/") for part in path.split("/")) or "/" def signed_headers(method: str, path: str, body: bytes): amz_date = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ") date_stamp = amz_date[:8] headers = [("host", parsed.netloc), ("x-amz-content-sha256", sha256_hex(body)), ("x-amz-date", amz_date)] canonical_headers = "".join(f"{name}:{value}\n" for name, value in headers) signed_names = ";".join(name for name, _ in headers) canonical_request = "\n".join([method, canonical_uri(path), "", canonical_headers, signed_names, sha256_hex(body)]) scope = f"{date_stamp}/{region}/s3/aws4_request" string_to_sign = "\n".join(["AWS4-HMAC-SHA256", amz_date, scope, hashlib.sha256(canonical_request.encode()).hexdigest()]) signature = hmac.new(signing_key(secret_key, date_stamp), string_to_sign.encode(), hashlib.sha256).hexdigest() result = {name: value for name, value in headers} result["Authorization"] = ( "AWS4-HMAC-SHA256 " f"Credential={access_key}/{scope}, " f"SignedHeaders={signed_names}, " f"Signature={signature}" ) return result for bucket in pathlib.Path(bucket_file).read_text(encoding="utf-8").splitlines(): if not bucket: continue path = "/" + urllib.parse.quote(bucket, safe="") conn = http.client.HTTPConnection(parsed.hostname, parsed.port or 80, timeout=30) conn.request("PUT", path, body=b"", headers=signed_headers("PUT", path, b"")) response = conn.getresponse() response.read() conn.close() if response.status not in (200, 409): raise SystemExit(f"create benchmark bucket failed: {bucket}: {response.status} {response.reason}") PY } run_bench() { local profile="$1" local baseline_csv="${2:-}" local profile_dir="${OUT_DIR}/${profile}" local bench_dir="${profile_dir}/warp" local cmd=( "$ENHANCED_BENCH" --tool warp --endpoint "$(endpoint_url)" --access-key "$ACCESS_KEY" --secret-key "$SECRET_KEY" --bucket "$BUCKET" --region "$REGION" --warp-bin "$WARP_BIN" --warp-mode "$WARP_MODE" --sizes "$SIZES" --concurrency "$CONCURRENCY" --duration "$DURATION" --rounds "$ROUNDS" --retry-per-round "$RETRY_PER_ROUND" --round-cooldown-secs "$ROUND_COOLDOWN_SECS" --out-dir "$bench_dir" ) if [[ -n "$baseline_csv" ]]; then cmd+=(--baseline-csv "$baseline_csv") fi local lifecycle_args="" if [[ "$WARP_OBJECT_LIFECYCLE" == "per-round" && -n "$WARP_OBJECTS" ]]; then lifecycle_args="--objects ${WARP_OBJECTS}" elif [[ "$WARP_OBJECT_LIFECYCLE" != "per-round" ]]; then lifecycle_args="--list-existing --noclear" if [[ -n "$WARP_OBJECTS" ]]; then lifecycle_args="${lifecycle_args} --objects ${WARP_OBJECTS}" fi if [[ "$(benchmark_size_count)" -gt 1 ]]; then cmd+=(--bucket-size-suffix) fi fi if [[ -n "$WARP_EXTRA_ARGS" ]]; then lifecycle_args="${lifecycle_args:+${lifecycle_args} }${WARP_EXTRA_ARGS}" fi if [[ -n "$lifecycle_args" ]]; then cmd+=(--extra-args "$lifecycle_args") fi if [[ "$DIAGNOSTIC_METRICS" == "true" && -z "$DIAGNOSTIC_PROMETHEUS_QUERY_URL" ]]; then cmd+=( --service-metrics-url "$DIAGNOSTIC_METRICS_URL" --service-metrics-dir "${profile_dir}/service-metrics/rounds" --service-metrics-attempts "$DIAGNOSTIC_METRICS_CAPTURE_ATTEMPTS" --service-metrics-retry-secs "$DIAGNOSTIC_METRICS_CAPTURE_RETRY_SECS" --service-metrics-connect-timeout-secs "$DIAGNOSTIC_METRICS_CONNECT_TIMEOUT_SECS" --service-metrics-max-time-secs "$DIAGNOSTIC_METRICS_MAX_TIME_SECS" --service-metrics-filter-regex "$DIAGNOSTIC_METRICS_FILTER_REGEX" ) fi if [[ "$DRY_RUN" == "true" ]]; then cmd+=(--dry-run) fi log "Running ${profile} GET benchmark..." "${cmd[@]}" } write_metrics_summary() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local median_csv="${profile_dir}/warp/median_summary.csv" if [[ -f "$median_csv" ]]; then cp "$median_csv" "${profile_dir}/metrics_summary.csv" fi } service_metrics_dir() { local profile="$1" echo "${OUT_DIR}/${profile}/service-metrics" } capture_prometheus_query_snapshot() { local phase="$1" local snapshot_file="$2" local status_file="$3" local service_name="$4" "$PYTHON_BIN" - "$DIAGNOSTIC_PROMETHEUS_QUERY_URL" "$DIAGNOSTIC_PROMETHEUS_QUERY" "$phase" "$snapshot_file" "$status_file" "$service_name" <<'PY' import json import pathlib import sys import urllib.parse import urllib.request query_url, query, phase, snapshot_raw, status_raw, service_name = sys.argv[1:] snapshot_path = pathlib.Path(snapshot_raw) status_path = pathlib.Path(status_raw) def write_status(status): status_path.write_text( "\n".join( [ f"phase={phase}", f"status={status}", f"url={query_url}", f"query={query}", f"service_name={service_name}", "", ] ), encoding="utf-8", ) def escape_label(value): return value.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"') def sample_to_line(sample): metric = dict(sample.get("metric", {})) name = metric.pop("__name__", "") if not name: return None labels = ",".join(f'{key}="{escape_label(value)}"' for key, value in sorted(metric.items())) value = sample.get("value", [None, None])[1] if value is None: return None if labels: return f"{name}{{{labels}}} {value}" return f"{name} {value}" try: separator = "&" if "?" in query_url else "?" url = f"{query_url}{separator}{urllib.parse.urlencode({'query': query})}" request = urllib.request.Request(url, headers={"Accept": "application/json"}) with urllib.request.urlopen(request, timeout=5) as response: payload = json.loads(response.read().decode("utf-8")) except Exception as err: # noqa: BLE001 - shell harness reports the failure in status files. snapshot_path.write_text(f"# prometheus_query_failed error={err}\n", encoding="utf-8") write_status("query_failed") raise SystemExit(0) if payload.get("status") != "success": snapshot_path.write_text(f"# prometheus_query_failed payload_status={payload.get('status')}\n", encoding="utf-8") write_status("query_failed") raise SystemExit(0) samples = [ sample for sample in payload.get("data", {}).get("result", []) if not service_name or sample.get("metric", {}).get("service.name") == service_name or sample.get("metric", {}).get("service_name") == service_name ] lines = [line for sample in samples if (line := sample_to_line(sample))] if not lines: snapshot_path.write_text(f"# no_matching_metrics service_name={service_name}\n", encoding="utf-8") write_status("no_matching_metrics") raise SystemExit(0) snapshot_path.write_text("\n".join(lines) + "\n", encoding="utf-8") write_status("ok") PY } filter_diagnostic_metrics_snapshot() { local input_file="$1" local output_file="$2" if [[ -z "$DIAGNOSTIC_METRICS_FILTER_REGEX" ]]; then mv "$input_file" "$output_file" return 0 fi awk -v pattern="$DIAGNOSTIC_METRICS_FILTER_REGEX" '$0 ~ pattern { print }' "$input_file" >"$output_file" if [[ ! -s "$output_file" ]]; then mv "$input_file" "$output_file" else rm -f "$input_file" fi } capture_service_metrics_snapshot() { local profile="$1" local phase="$2" local metrics_dir snapshot_file status_file metrics_dir="$(service_metrics_dir "$profile")" snapshot_file="${metrics_dir}/${phase}.prom" status_file="${metrics_dir}/${phase}.status" if [[ "$DIAGNOSTIC_METRICS" != "true" ]]; then return 0 fi mkdir -p "$metrics_dir" if [[ "$DRY_RUN" == "true" ]]; then : >"$snapshot_file" cat >"$status_file" <"$tmp_file"; then if [[ -s "$tmp_file" ]]; then raw_bytes="$(wc -c <"$tmp_file" | tr -d '[:space:]')" filter_diagnostic_metrics_snapshot "$tmp_file" "$filtered_file" mv "$filtered_file" "$snapshot_file" snapshot_bytes="$(wc -c <"$snapshot_file" | tr -d '[:space:]')" cat >"$status_file" < 0 )); then sleep "$DIAGNOSTIC_METRICS_CAPTURE_RETRY_SECS" fi done : >"$snapshot_file" cat >"$status_file" <"$out_csv" <"$out_csv" <"$out_csv" <"$out_summary" <"$out_distribution" <"$out_percentiles" <"$out_summary" <"$out_distribution" <"$out_percentiles" <"$out_summary" <"$out_distribution" <"$out_percentiles" <= threshold: return le return finite_or_inf[-1][0] def metric_count_sum(before, after, metric_name, path="", stage="", status=""): count = 0.0 total = 0.0 for metric, labels in sorted(set(before) | set(after)): if path and label_value(labels, "path") != path: continue if stage and label_value(labels, "stage") != stage: continue if status and label_value(labels, "status") != status: continue if metric == f"{metric_name}_count": count += after.get((metric, labels), 0.0) - before.get((metric, labels), 0.0) elif metric == f"{metric_name}_sum": total += after.get((metric, labels), 0.0) - before.get((metric, labels), 0.0) return count, total PERCENTILE_TARGETS = [ ("request_duration_ok", "rustfs_io_get_object_request_duration_seconds", "", "", "ok"), ("total_duration", "rustfs_io_get_object_total_duration_seconds", "", "", ""), ("request_context", "rustfs_io_get_object_stage_duration_seconds", "s3_handler", "request_context", ""), ] for stage_name in TARGET_STAGES: PERCENTILE_TARGETS.append((stage_name, "rustfs_io_get_object_stage_duration_seconds", "", stage_name, "")) with summary_path.open("w", encoding="utf-8", newline="") as summary_handle, \ distribution_path.open("w", encoding="utf-8", newline="") as distribution_handle, \ percentiles_path.open("w", encoding="utf-8", newline="") as percentiles_handle: summary_writer = csv.writer(summary_handle) distribution_writer = csv.writer(distribution_handle) percentiles_writer = csv.writer(percentiles_handle) summary_writer.writerow([ "profile", "size", "tool", "round", "attempt", "round_status", "stage", "count_delta", "sum_delta", "avg_ms", "minor_fault_delta", "major_fault_delta", "minor_faults_per_call", "major_faults_per_call", "before_status", "after_status", ]) distribution_writer.writerow([ "profile", "size", "tool", "round", "attempt", "round_status", "path", "stage", "le", "bucket_delta", "before_status", "after_status", ]) percentiles_writer.writerow([ "profile", "size", "tool", "round", "attempt", "round_status", "target", "path", "stage", "count_delta", "sum_delta", "avg_ms", "p50_le_seconds", "p90_le_seconds", "p99_le_seconds", "bucket_resolution_note", "before_status", "after_status", ]) if not round_path.exists() or not metrics_dir.exists(): summary_writer.writerow([ profile, "N/A", "N/A", "N/A", "N/A", "snapshot_missing", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "missing", "missing", ]) distribution_writer.writerow([ profile, "N/A", "N/A", "N/A", "N/A", "snapshot_missing", "N/A", "N/A", "N/A", "N/A", "missing", "missing", ]) percentiles_writer.writerow([ profile, "N/A", "N/A", "N/A", "N/A", "snapshot_missing", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A", "snapshot_missing", "missing", "missing", ]) raise SystemExit(0) for round_row in csv.DictReader(round_path.open(encoding="utf-8", newline="")): size = round_row["size"] tool = round_row["tool"] round_id = round_row["round"] attempt = round_row["attempt"] round_status = round_row["status"] token = snapshot_token(tool, size, round_id, attempt) before_path = metrics_dir / f"{token}_before.prom" after_path = metrics_dir / f"{token}_after.prom" before_status = status_for(metrics_dir / f"{token}_before.status") after_status = status_for(metrics_dir / f"{token}_after.status") before = read_metrics(before_path) after = read_metrics(after_path) minor_faults = fault_delta(before, after, "minor") major_faults = fault_delta(before, after, "major") for stage in TARGET_STAGES: count_delta = metric_delta(before, after, "rustfs_io_get_object_stage_duration_seconds", stage, "count") sum_delta = metric_delta(before, after, "rustfs_io_get_object_stage_duration_seconds", stage, "sum") avg_ms = (sum_delta / count_delta * 1000.0) if count_delta else 0.0 stage_minor_faults = minor_faults if stage == "reader_mmap_copy_buffer" else 0.0 stage_major_faults = major_faults if stage == "reader_mmap_copy_buffer" else 0.0 summary_writer.writerow([ profile, size, tool, round_id, attempt, round_status, stage, f"{count_delta:.12g}", f"{sum_delta:.12g}", f"{avg_ms:.6f}", f"{stage_minor_faults:.12g}", f"{stage_major_faults:.12g}", f"{(stage_minor_faults / count_delta) if count_delta else 0.0:.6f}", f"{(stage_major_faults / count_delta) if count_delta else 0.0:.6f}", before_status, after_status, ]) for target, metric_name, path_label, stage_label, status_label in PERCENTILE_TARGETS: buckets = histogram_bucket_deltas(before, after, metric_name, path_label, stage_label, status_label) count_delta, sum_delta = metric_count_sum(before, after, metric_name, path_label, stage_label, status_label) avg_ms = (sum_delta / count_delta * 1000.0) if count_delta else 0.0 p50 = percentile_bucket_upper_bound(buckets, 0.50) p90 = percentile_bucket_upper_bound(buckets, 0.90) p99 = percentile_bucket_upper_bound(buckets, 0.99) if not buckets: note = "no_buckets" elif p50 == p90 == p99 and p99 not in {"N/A", "+Inf"} and bucket_sort_key(p99) >= 1.0 and avg_ms < 1000.0: note = "coarse_seconds_buckets" else: note = "bucket_upper_bound" percentiles_writer.writerow([ profile, size, tool, round_id, attempt, round_status, target, path_label or "N/A", stage_label or "N/A", f"{count_delta:.12g}", f"{sum_delta:.12g}", f"{avg_ms:.6f}", p50, p90, p99, note, before_status, after_status, ]) for metric, labels in sorted(set(before) | set(after)): if metric != "rustfs_io_get_object_stage_duration_seconds_bucket": continue stage = label_value(labels, "stage") if stage not in DISTRIBUTION_STAGES: continue bucket_delta = after.get((metric, labels), 0.0) - before.get((metric, labels), 0.0) if bucket_delta == 0: continue distribution_writer.writerow([ profile, size, tool, round_id, attempt, round_status, label_value(labels, "path"), stage, label_value(labels, "le"), f"{bucket_delta:.12g}", before_status, after_status, ]) PY } write_root_service_metrics_summary() { local out_csv="${OUT_DIR}/service_metrics_summary.csv" local profile summary wrote_header=false : >"$out_csv" for profile in "$@"; do summary="${OUT_DIR}/${profile}/service_metrics_summary.csv" [[ -f "$summary" ]] || continue if [[ "$wrote_header" == "false" ]]; then cat "$summary" >>"$out_csv" wrote_header=true else tail -n +2 "$summary" >>"$out_csv" fi done if [[ "$wrote_header" == "false" ]]; then cat >"$out_csv" <<'EOF' profile,status,metric,labels,before,after,delta,classification N/A,missing,N/A,N/A,N/A,N/A,N/A,no_profile_service_metrics_summary EOF fi } write_root_profile_csv() { local basename="$1" shift local out_csv="${OUT_DIR}/${basename}" local profile summary wrote_header=false : >"$out_csv" for profile in "$@"; do summary="${OUT_DIR}/${profile}/${basename}" [[ -f "$summary" ]] || continue if [[ "$wrote_header" == "false" ]]; then cat "$summary" >>"$out_csv" wrote_header=true else tail -n +2 "$summary" >>"$out_csv" fi done if [[ "$wrote_header" == "false" ]]; then cat >"$out_csv" <<'EOF' profile,status,note N/A,missing,no_profile_csv EOF fi } write_service_metrics_acceptance() { local out_csv="${OUT_DIR}/service_metrics_acceptance.csv" local service_csv="${OUT_DIR}/service_metrics_summary.csv" "$PYTHON_BIN" - "$out_csv" "$service_csv" "$DIAGNOSTIC_METRICS" "$MODE" "$CODEC_ENGINES" \ "$COMPRESSED_FALLBACK_PROBE" "${OUT_DIR}/metrics_summary.csv" "$CODEC_MULTIPART" \ "$CODEC_MULTIPART_MAX_PARTS" <<'PY' import csv import pathlib import sys out_path = pathlib.Path(sys.argv[1]) service_path = pathlib.Path(sys.argv[2]) diagnostic_enabled = sys.argv[3] == "true" mode = sys.argv[4] codec_engines = [item.strip() for item in sys.argv[5].split(",") if item.strip()] compressed_fallback_probe_enabled = sys.argv[6] == "true" metrics_path = pathlib.Path(sys.argv[7]) codec_multipart_enabled = sys.argv[8] == "on" codec_multipart_max_parts = int(sys.argv[9]) def expected_profiles(): codec_profiles = [f"codec-{engine}" for engine in codec_engines] if mode == "legacy": return ["legacy"] if mode == "codec": return codec_profiles return ["legacy", *codec_profiles] rows = [] if service_path.exists(): with service_path.open(encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle)) metrics_rows = [] if metrics_path.exists(): with metrics_path.open(encoding="utf-8", newline="") as handle: metrics_rows = list(csv.DictReader(handle)) def metric_matches(actual, expected): return actual == expected or actual == f"{expected}_total" def delta_for(profile, metric, *label_fragments): total = 0.0 for row in rows: if row.get("profile") != profile or row.get("status") != "ok": continue if not metric_matches(row.get("metric", ""), metric): continue labels = row.get("labels", "") if all(fragment in labels for fragment in label_fragments): try: total += float(row.get("delta", "0") or 0) except ValueError: pass return total def delta_prefix(profile, metric_prefix, *label_fragments): total = 0.0 for row in rows: if row.get("profile") != profile or row.get("status") != "ok": continue if not row.get("metric", "").startswith(metric_prefix): continue labels = row.get("labels", "") if all(fragment in labels for fragment in label_fragments): try: total += float(row.get("delta", "0") or 0) except ValueError: pass return total def status_for(delta): return "pass" if delta > 0 else "fail" def add(profile, check, expected, delta, note): rows_out.append( { "profile": profile, "check": check, "expected": expected, "status": status_for(delta), "observed_delta": f"{delta:.12g}", "note": note, } ) def p99_available(): for row in metrics_rows: value = row.get("median_req_p99_ms", "") if value and value != "N/A": return True return False rows_out = [] if not diagnostic_enabled: rows_out.append( { "profile": "N/A", "check": "diagnostic_metrics_enabled", "expected": "true", "status": "skipped", "observed_delta": "N/A", "note": "performance run: observability metrics export intentionally disabled", } ) elif rows and all(row.get("status") == "not_run_dry_run" for row in rows): rows_out.append( { "profile": "N/A", "check": "diagnostic_metrics_enabled", "expected": "non-dry-run service metrics snapshots", "status": "skipped", "observed_delta": "N/A", "note": "dry-run only validates artifact wiring; runtime deltas require a real server run", } ) elif rows and all(row.get("status") == "snapshot_missing" for row in rows): rows_out.append( { "profile": "N/A", "check": "diagnostic_metrics_capture", "expected": "non-empty service metrics snapshots", "status": "fail", "observed_delta": "N/A", "note": "metrics endpoint returned empty or missing snapshots; verify --diagnostic-metrics-url and exporter setup", } ) else: for profile in expected_profiles(): if profile == "legacy": add( profile, "reader_path", 'rustfs_io_get_object_reader_path_total{path="legacy_duplex"} delta > 0', delta_for(profile, "rustfs_io_get_object_reader_path_total", 'path="legacy_duplex"'), "proves legacy_duplex path was exercised", ) add( profile, "fallback_disabled", 'rustfs_io_get_object_codec_streaming_fallback_total{reason="disabled"} delta > 0', delta_for(profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="disabled"'), "proves kill-switch fallback reason at runtime", ) else: engine = profile[len("codec-") :] if profile.startswith("codec-") else profile engine_path = "codec_streaming_rustfs_engine" if engine == "rustfs" else "codec_streaming_legacy_engine" add( profile, "codec_decision_use", 'rustfs_io_get_object_codec_streaming_decision_total{outcome="use",reason="none"} delta > 0', delta_for( profile, "rustfs_io_get_object_codec_streaming_decision_total", 'outcome="use"', 'reason="none"', ), "proves eligible GETs selected codec streaming", ) add( profile, "reader_path", 'rustfs_io_get_object_reader_path_total{path="codec_streaming"} delta > 0', delta_for(profile, "rustfs_io_get_object_reader_path_total", 'path="codec_streaming"'), "proves response reader used the codec streaming path", ) add( profile, "engine_stage", f'rustfs_io_get_object_stage_duration_seconds*{{path="{engine_path}"}} delta > 0', delta_prefix(profile, "rustfs_io_get_object_stage_duration_seconds", f'path="{engine_path}"'), "proves engine-specific codec path stage attribution", ) add( profile, "reader_bytes", f'rustfs_io_get_object_reader_bytes_total{{path="{engine_path}"}} delta > 0', delta_for(profile, "rustfs_io_get_object_reader_bytes_total", f'path="{engine_path}"'), "proves emitted reader bytes were attributed to the selected engine", ) add( profile, "fallback_range", 'rustfs_io_get_object_codec_streaming_fallback_total{reason="range"} delta > 0', delta_for(profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="range"'), "proves runtime Range GET fallback reason delta", ) if codec_multipart_enabled: if codec_multipart_max_parts < 2: add( profile, "multipart_fallback_part_limit", 'rustfs_io_get_object_codec_streaming_decision_total{outcome="fallback",object_class="multipart",reason="multipart_part_limit"} delta > 0', delta_for( profile, "rustfs_io_get_object_codec_streaming_decision_total", 'outcome="fallback"', 'object_class="multipart"', 'reason="multipart_part_limit"', ), "proves multipart codec streaming falls back when part count exceeds the configured guard", ) else: add( profile, "multipart_codec_decision_use", 'rustfs_io_get_object_codec_streaming_decision_total{outcome="use",object_class="multipart",reason="none"} delta > 0', delta_for( profile, "rustfs_io_get_object_codec_streaming_decision_total", 'outcome="use"', 'object_class="multipart"', 'reason="none"', ), "proves runtime multipart GET selected codec streaming under explicit opt-in", ) add( profile, "multipart_fallback_read_quorum_not_safe", 'rustfs_io_get_object_codec_streaming_decision_total{outcome="fallback",object_class="multipart",reason="read_quorum_not_safe"} delta > 0', delta_for( profile, "rustfs_io_get_object_codec_streaming_decision_total", 'outcome="fallback"', 'object_class="multipart"', 'reason="read_quorum_not_safe"', ), "proves unsafe multipart part setup falls back before returning a codec reader", ) else: add( profile, "fallback_multipart", 'rustfs_io_get_object_codec_streaming_fallback_total{reason="multipart"} delta > 0', delta_for(profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="multipart"'), "proves runtime multipart object fallback reason delta", ) add( profile, "fallback_encrypted", 'rustfs_io_get_object_codec_streaming_fallback_total{reason="encrypted"} delta > 0', delta_for(profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="encrypted"'), "proves runtime encrypted object fallback reason delta", ) add( profile, "fallback_read_quorum_not_safe", 'rustfs_io_get_object_codec_streaming_fallback_total{reason="read_quorum_not_safe"} delta > 0', delta_for( profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="read_quorum_not_safe"', ), "proves degraded-but-readable shard setup uses the legacy fallback path", ) compressed_delta = delta_for( profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="compressed"', ) rows_out.append( { "profile": profile, "check": "fallback_compressed", "expected": 'rustfs_io_get_object_codec_streaming_fallback_total{reason="compressed"} delta > 0', "status": "pass" if compressed_delta > 0 else ("fail" if compressed_fallback_probe_enabled else "unavailable"), "observed_delta": f"{compressed_delta:.12g}", "note": "Enable --compressed-fallback-probe to generate a runtime compressed fallback delta" if not compressed_fallback_probe_enabled else "proves runtime disk-compressed object fallback reason delta", } ) below_delta = delta_for( profile, "rustfs_io_get_object_codec_streaming_fallback_total", 'reason="below_min_size"', ) rows_out.append( { "profile": profile, "check": "fallback_below_min_size", "expected": 'rustfs_io_get_object_codec_streaming_fallback_total{reason="below_min_size"} delta > 0', "status": "pass" if below_delta > 0 else "unavailable", "observed_delta": f"{below_delta:.12g}", "note": "Set --codec-min-size greater than 1 to force this runtime fallback probe when unavailable", } ) rows_out.append( { "profile": "all", "check": "diagnostic_vs_performance_separation", "expected": "diagnostic artifacts are separate from performance conclusions", "status": "pass", "observed_delta": "N/A", "note": "use --diagnostic-metrics for path proof; omit it for throughput acceptance runs", } ) p99_status = p99_available() rows_out.append( { "profile": "all", "check": "p95_p99", "expected": "p99 available from warp request percentile output; p95 explicitly marked unavailable when warp does not emit it", "status": "pass" if p99_status else "unavailable", "observed_delta": "p99_available" if p99_status else "N/A", "note": "median_req_p99_ms is aggregated in metrics_summary.csv; p95 remains unavailable because current warp text output emits 90% and 99%, not 95%", } ) with out_path.open("w", encoding="utf-8", newline="") as handle: fieldnames = ["profile", "check", "expected", "status", "observed_delta", "note"] writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows_out) PY } start_server_sampler() { local pid="$1" local profile_dir="$2" SERVER_SAMPLER_LOG="${profile_dir}/resource_samples.csv" if [[ "$DRY_RUN" == "true" ]]; then cat >"$SERVER_SAMPLER_LOG" <<'EOF' timestamp_utc,rss_kib,cpu_pct,threads,fd_count,vm_hwm_kib,vm_data_kib,voluntary_ctxt_switches,nonvoluntary_ctxt_switches,sched_runtime_ns,sched_wait_ns,sched_timeslices DRY_RUN,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A,N/A EOF return fi echo "timestamp_utc,rss_kib,cpu_pct,threads,fd_count,vm_hwm_kib,vm_data_kib,voluntary_ctxt_switches,nonvoluntary_ctxt_switches,sched_runtime_ns,sched_wait_ns,sched_timeslices" >"$SERVER_SAMPLER_LOG" ( while kill -0 "$pid" >/dev/null 2>&1; do local timestamp sample rss_cpu threads fd_count vm_hwm vm_data voluntary_ctxt nonvoluntary_ctxt sched_runtime sched_wait sched_slices timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" rss_cpu="$(ps -o rss= -o %cpu= -p "$pid" 2>/dev/null | awk 'NF >= 2 { gsub(/^[ \t]+|[ \t]+$/, "", $1); gsub(/^[ \t]+|[ \t]+$/, "", $2); print $1 "," $2; exit }')" if [[ -z "$rss_cpu" ]]; then sleep "$RESOURCE_SAMPLE_INTERVAL_SECS" continue fi threads="N/A" fd_count="N/A" vm_hwm="N/A" vm_data="N/A" voluntary_ctxt="N/A" nonvoluntary_ctxt="N/A" sched_runtime="N/A" sched_wait="N/A" sched_slices="N/A" if [[ -r "/proc/${pid}/status" ]]; then sample="$(awk ' /^Threads:/ { threads=$2 } /^VmHWM:/ { vm_hwm=$2 } /^VmData:/ { vm_data=$2 } /^voluntary_ctxt_switches:/ { voluntary=$2 } /^nonvoluntary_ctxt_switches:/ { nonvoluntary=$2 } END { printf "%s,%s,%s,%s,%s", threads ? threads : "N/A", vm_hwm ? vm_hwm : "N/A", vm_data ? vm_data : "N/A", voluntary ? voluntary : "N/A", nonvoluntary ? nonvoluntary : "N/A" } ' "/proc/${pid}/status")" IFS=',' read -r threads vm_hwm vm_data voluntary_ctxt nonvoluntary_ctxt <<<"$sample" fi if [[ -d "/proc/${pid}/fd" ]]; then fd_count="$(find "/proc/${pid}/fd" -maxdepth 1 -type l 2>/dev/null | awk 'END { print NR + 0 }')" fi if [[ -d "/proc/${pid}/task" ]]; then sample="$(awk ' { runtime += $1 wait += $2 slices += $3 seen = 1 } END { if (seen) { printf "%.0f %.0f %.0f", runtime, wait, slices } } ' /proc/"${pid}"/task/*/schedstat 2>/dev/null || true)" if [[ -n "$sample" ]]; then read -r sched_runtime sched_wait sched_slices <<<"$sample" fi fi echo "${timestamp},${rss_cpu},${threads},${fd_count},${vm_hwm},${vm_data},${voluntary_ctxt},${nonvoluntary_ctxt},${sched_runtime},${sched_wait},${sched_slices}" >>"$SERVER_SAMPLER_LOG" sleep "$RESOURCE_SAMPLE_INTERVAL_SECS" done ) & SERVER_SAMPLER_PID="$!" } write_profile_cpu_rss_notes() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local sample_csv="${profile_dir}/resource_samples.csv" local notes_file="${profile_dir}/cpu_rss_notes.txt" if [[ "$DRY_RUN" == "true" ]]; then cat >"$notes_file" <"$notes_file" < max_rss) max_rss = rss if (count == 0 || cpu > max_cpu) max_cpu = cpu if (threads > max_threads) max_threads = threads if (fd_count > max_fd_count) max_fd_count = fd_count if (vm_hwm > max_vm_hwm) max_vm_hwm = vm_hwm if (vm_data > max_vm_data) max_vm_data = vm_data if (voluntary >= 0) { if (!seen_voluntary) { first_voluntary = voluntary; seen_voluntary = 1 } last_voluntary = voluntary } if (nonvoluntary >= 0) { if (!seen_nonvoluntary) { first_nonvoluntary = nonvoluntary; seen_nonvoluntary = 1 } last_nonvoluntary = nonvoluntary } cpu_sum += cpu count++ } END { print "profile=" profile print "samples=" count + 0 if (count == 0) { print "max_rss_kib=unknown" print "max_cpu_pct=unknown" print "avg_cpu_pct=unknown" print "cpu_rss_acceptability=not_proven" print "note=No non-empty resource samples were captured." } else { printf "max_rss_kib=%.0f\n", max_rss printf "max_cpu_pct=%.2f\n", max_cpu printf "avg_cpu_pct=%.2f\n", cpu_sum / count printf "max_threads=%.0f\n", max_threads printf "max_fd_count=%.0f\n", max_fd_count printf "max_vm_hwm_kib=%.0f\n", max_vm_hwm printf "max_vm_data_kib=%.0f\n", max_vm_data if (seen_voluntary) { printf "voluntary_ctxt_switches_delta=%.0f\n", last_voluntary - first_voluntary } else { print "voluntary_ctxt_switches_delta=unknown" } if (seen_nonvoluntary) { printf "nonvoluntary_ctxt_switches_delta=%.0f\n", last_nonvoluntary - first_nonvoluntary } else { print "nonvoluntary_ctxt_switches_delta=unknown" } print "cpu_rss_acceptability=manual_review_required" print "note=Harness captured runtime/resource samples but does not impose a hard pass/fail threshold." } } ' "$sample_csv" >"$notes_file" } write_skipped_compat_probe() { local profile="$1" local profile_dir="${OUT_DIR}/${profile}" local compat_dir="${profile_dir}/compat" mkdir -p "$compat_dir" cat >"${compat_dir}/compat_summary.csv" <"${compat_dir}/fallback_probe_summary.csv" <"${compat_dir}/compat_summary.csv" <"${compat_dir}/body_sha256.txt" printf '{}\n' >"${compat_dir}/response_headers.json" { cat <"${compat_dir}/fallback_probe_summary.csv" cat >"${compat_dir}/snapshot.json" < bytes: return bytes(((index * 31 + 7) % 251 for index in range(size))) def sha256_hex(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sign(key: bytes, value: str) -> bytes: return hmac.new(key, value.encode(), hashlib.sha256).digest() def signing_key(secret: str, date_stamp: str) -> bytes: key_date = sign(("AWS4" + secret).encode(), date_stamp) key_region = sign(key_date, region) key_service = sign(key_region, "s3") return sign(key_service, "aws4_request") def canonical_query(query: str) -> str: pairs = urllib.parse.parse_qsl(query, keep_blank_values=True) encoded = [ (urllib.parse.quote(key, safe="-_.~"), urllib.parse.quote(value, safe="-_.~")) for key, value in pairs ] encoded.sort() return "&".join(f"{key}={value}" for key, value in encoded) def canonical_uri(path: str) -> str: return "/".join(urllib.parse.quote(part, safe="-_.~/") for part in path.split("/")) or "/" def signed_headers(method: str, path: str, body: bytes, extra_headers: List[Tuple[str, str]]) -> Dict[str, str]: parsed = urllib.parse.urlsplit(endpoint) amz_date = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ") date_stamp = amz_date[:8] headers = [("host", parsed.netloc), ("x-amz-content-sha256", sha256_hex(body)), ("x-amz-date", amz_date)] headers.extend((name.lower(), " ".join(value.strip().split())) for name, value in extra_headers) headers.sort() canonical_headers = "".join(f"{name}:{value}\n" for name, value in headers) signed_names = ";".join(name for name, _ in headers) raw_path, _, raw_query = path.partition("?") canonical_request = "\n".join( [method, canonical_uri(raw_path), canonical_query(raw_query), canonical_headers, signed_names, sha256_hex(body)] ) scope = f"{date_stamp}/{region}/s3/aws4_request" string_to_sign = "\n".join( ["AWS4-HMAC-SHA256", amz_date, scope, hashlib.sha256(canonical_request.encode()).hexdigest()] ) signature = hmac.new(signing_key(secret_key, date_stamp), string_to_sign.encode(), hashlib.sha256).hexdigest() result = {name: value for name, value in headers} result["Authorization"] = ( "AWS4-HMAC-SHA256 " f"Credential={access_key}/{scope}, " f"SignedHeaders={signed_names}, " f"Signature={signature}" ) return result def request(method: str, path: str, body: bytes = b"", extra_headers: Optional[List[Tuple[str, str]]] = None): parsed = urllib.parse.urlsplit(endpoint) headers = signed_headers(method, path, body, extra_headers or []) if body and "content-length" not in headers: headers["content-length"] = str(len(body)) conn = http.client.HTTPConnection(parsed.hostname, parsed.port or 80, timeout=30) conn.request(method, path, body=body, headers=headers) response = conn.getresponse() response_body = response.read() snapshot = { "status": response.status, "reason": response.reason, "headers": response.getheaders(), "body_sha256": sha256_hex(response_body), "body_len": len(response_body), } conn.close() return snapshot, response_body def header_map(headers) -> Dict[str, List[str]]: result: Dict[str, List[str]] = {} for name, value in headers: result.setdefault(name.lower(), []).append(value) for value in result.values(): value.sort() return dict(sorted(result.items())) def first_header(snapshot, name: str) -> str: name = name.lower() for header_name, value in snapshot["headers"]: if header_name.lower() == name: return value return "" def find_xml_text(body: bytes, name: str) -> Optional[str]: try: root = ET.fromstring(body) except ET.ParseError: return None for elem in root.iter(): if elem.tag.rsplit("}", 1)[-1] == name: return elem.text return None bucket_path = "/" + urllib.parse.quote(bucket, safe="") object_path = bucket_path + "/" + urllib.parse.quote(object_key, safe="/-_.~") body = payload(object_size) create_bucket, _ = request("PUT", bucket_path) if create_bucket["status"] not in (200, 409): raise SystemExit(f"create bucket failed: {create_bucket['status']} {create_bucket['reason']}") put_object, _ = request("PUT", object_path, body, [("content-type", "application/octet-stream")]) if put_object["status"] not in (200, 204): raise SystemExit(f"put object failed: {put_object['status']} {put_object['reason']}") head_object, _ = request("HEAD", object_path) get_object, get_body = request("GET", object_path) if get_object["status"] != 200: raise SystemExit(f"get object failed: {get_object['status']} {get_object['reason']}") range_object, range_body = request("GET", object_path, extra_headers=[("range", "bytes=0-0")]) fallback_rows = [ { "profile": profile, "probe": "range", "object_key": object_key, "status": "ok" if range_object["status"] == 206 else "unexpected_status", "status_code": range_object["status"], "body_len": len(range_body), "expected_runtime_fallback_reason": "range", "note": "Range GET should stay on the legacy fallback path for codec profiles", } ] if codec_min_size > 1: small_size = max(1, min(codec_min_size - 1, object_size)) small_key = object_key + ".below-min-size" small_path = bucket_path + "/" + urllib.parse.quote(small_key, safe="/-_.~") small_body = payload(small_size) small_put, _ = request("PUT", small_path, small_body, [("content-type", "application/octet-stream")]) if small_put["status"] not in (200, 204): fallback_rows.append( { "profile": profile, "probe": "below_min_size", "object_key": small_key, "status": "put_failed", "status_code": small_put["status"], "body_len": 0, "expected_runtime_fallback_reason": "below_min_size", "note": "below-min-size probe upload failed", } ) else: small_get, small_get_body = request("GET", small_path) fallback_rows.append( { "profile": profile, "probe": "below_min_size", "object_key": small_key, "status": "ok" if small_get["status"] == 200 else "unexpected_status", "status_code": small_get["status"], "body_len": len(small_get_body), "expected_runtime_fallback_reason": "below_min_size", "note": "Object smaller than codec-min-size should stay on the legacy fallback path for codec profiles", } ) else: fallback_rows.append( { "profile": profile, "probe": "below_min_size", "object_key": object_key + ".below-min-size", "status": "skipped", "status_code": "N/A", "body_len": "N/A", "expected_runtime_fallback_reason": "below_min_size", "note": "Set --codec-min-size greater than 1 to generate a runtime below_min_size fallback delta", } ) compressed_key = object_key + compressed_probe_extension compressed_path = bucket_path + "/" + urllib.parse.quote(compressed_key, safe="/-_.~") if compressed_fallback_probe: compressed_body = (b"RustFS compressed fallback probe\n" * 4096)[:128 * 1024] compressed_put, _ = request( "PUT", compressed_path, compressed_body, [("content-type", compressed_probe_mime_type)], ) if compressed_put["status"] not in (200, 204): fallback_rows.append( { "profile": profile, "probe": "compressed", "object_key": compressed_key, "status": "put_failed", "status_code": compressed_put["status"], "body_len": 0, "expected_runtime_fallback_reason": "compressed", "note": "compressed probe upload failed", } ) else: compressed_get, compressed_get_body = request("GET", compressed_path) compressed_ok = compressed_get["status"] == 200 and sha256_hex(compressed_get_body) == sha256_hex(compressed_body) fallback_rows.append( { "profile": profile, "probe": "compressed", "object_key": compressed_key, "status": "ok" if compressed_ok else "unexpected_status_or_body", "status_code": compressed_get["status"], "body_len": len(compressed_get_body), "expected_runtime_fallback_reason": "compressed", "note": "Disk-compressed object GET should stay on the legacy fallback path for codec profiles", } ) else: fallback_rows.append( { "profile": profile, "probe": "compressed", "object_key": compressed_key, "status": "skipped", "status_code": "N/A", "body_len": "N/A", "expected_runtime_fallback_reason": "compressed", "note": "Enable --compressed-fallback-probe to generate a runtime compressed fallback delta", } ) degraded_key = object_key + ".degraded-read" degraded_path = bucket_path + "/" + urllib.parse.quote(degraded_key, safe="/-_.~") degraded_body = payload(max(object_size, 8 * 1024 * 1024)) degraded_put, _ = request("PUT", degraded_path, degraded_body, [("content-type", "application/octet-stream")]) if degraded_put["status"] not in (200, 204): fallback_rows.append( { "profile": profile, "probe": "read_quorum_not_safe", "object_key": degraded_key, "status": "put_failed", "status_code": degraded_put["status"], "body_len": 0, "expected_runtime_fallback_reason": "read_quorum_not_safe", "note": "degraded-read probe upload failed", } ) else: profile_dir = out_dir.parent data_root = profile_dir / "data" degraded_rel = pathlib.Path(*degraded_key.split("/")) part_candidates = [] for disk_dir in sorted(data_root.glob("disk*")): object_dir = disk_dir / bucket / degraded_rel part_candidates.extend(sorted(object_dir.glob("*/part.1"))) if not part_candidates: fallback_rows.append( { "profile": profile, "probe": "read_quorum_not_safe", "object_key": degraded_key, "status": "part_file_missing", "status_code": "N/A", "body_len": 0, "expected_runtime_fallback_reason": "read_quorum_not_safe", "note": "degraded-read probe could not find an on-disk part.1 shard to move", } ) else: degraded_part = part_candidates[0] missing_dir = out_dir / "degraded-missing-shards" missing_dir.mkdir(parents=True, exist_ok=True) missing_part = missing_dir / f"{degraded_part.parent.name}-{degraded_part.name}" if missing_part.exists(): missing_part.unlink() degraded_part.rename(missing_part) try: degraded_part.parent.rmdir() except OSError: pass degraded_get, degraded_get_body = request("GET", degraded_path) degraded_ok = degraded_get["status"] == 200 and sha256_hex(degraded_get_body) == sha256_hex(degraded_body) fallback_rows.append( { "profile": profile, "probe": "read_quorum_not_safe", "object_key": degraded_key, "status": "ok" if degraded_ok else "unexpected_status_or_body", "status_code": degraded_get["status"], "body_len": len(degraded_get_body), "expected_runtime_fallback_reason": "read_quorum_not_safe", "note": f"Moved one shard to {missing_part}; degraded GET should stay on the legacy fallback path for codec profiles", } ) multipart_key = object_key + ".multipart" multipart_path = bucket_path + "/" + urllib.parse.quote(multipart_key, safe="/-_.~") multipart_part_one = payload(5 * 1024 * 1024) multipart_part_two = payload(1) multipart_expected_reason = "multipart" if codec_multipart_enabled and profile.startswith("codec-"): multipart_expected_reason = "multipart_part_limit" if codec_multipart_max_parts < 2 else "none" multipart_note = ( "Multipart object GET should use the codec streaming path for codec profiles" if multipart_expected_reason == "none" else "Multipart object GET should exceed the codec streaming part-count guard" if multipart_expected_reason == "multipart_part_limit" else "Multipart object GET should stay on the legacy fallback path for codec profiles" ) initiate_multipart, initiate_multipart_body = request("POST", multipart_path + "?uploads") if initiate_multipart["status"] not in (200, 201): fallback_rows.append( { "profile": profile, "probe": "multipart", "object_key": multipart_key, "status": "initiate_failed", "status_code": initiate_multipart["status"], "body_len": 0, "expected_runtime_fallback_reason": multipart_expected_reason, "note": "multipart initiate failed", } ) else: upload_id = find_xml_text(initiate_multipart_body, "UploadId") if not upload_id: fallback_rows.append( { "profile": profile, "probe": "multipart", "object_key": multipart_key, "status": "upload_id_missing", "status_code": initiate_multipart["status"], "body_len": 0, "expected_runtime_fallback_reason": multipart_expected_reason, "note": "multipart initiate response did not include UploadId", } ) else: upload_id_query = urllib.parse.quote(upload_id, safe="") part_one, _ = request("PUT", f"{multipart_path}?partNumber=1&uploadId={upload_id_query}", multipart_part_one) part_two, _ = request("PUT", f"{multipart_path}?partNumber=2&uploadId={upload_id_query}", multipart_part_two) etag_one = first_header(part_one, "etag") etag_two = first_header(part_two, "etag") if part_one["status"] not in (200, 201) or part_two["status"] not in (200, 201) or not etag_one or not etag_two: fallback_rows.append( { "profile": profile, "probe": "multipart", "object_key": multipart_key, "status": "upload_part_failed", "status_code": f"{part_one['status']};{part_two['status']}", "body_len": 0, "expected_runtime_fallback_reason": multipart_expected_reason, "note": "multipart part upload failed or ETag was missing", } ) request("DELETE", f"{multipart_path}?uploadId={upload_id_query}") else: complete_body = ( '' "" f"1{xml_escape(etag_one)}" f"2{xml_escape(etag_two)}" "" ).encode() complete_multipart, _ = request( "POST", f"{multipart_path}?uploadId={upload_id_query}", complete_body, [("content-type", "application/xml")], ) if complete_multipart["status"] not in (200, 201): fallback_rows.append( { "profile": profile, "probe": "multipart", "object_key": multipart_key, "status": "complete_failed", "status_code": complete_multipart["status"], "body_len": 0, "expected_runtime_fallback_reason": multipart_expected_reason, "note": "multipart complete failed", } ) request("DELETE", f"{multipart_path}?uploadId={upload_id_query}") else: multipart_get, multipart_get_body = request("GET", multipart_path) expected_multipart_len = len(multipart_part_one) + len(multipart_part_two) expected_multipart_body_hash = sha256_hex(multipart_part_one + multipart_part_two) multipart_ok = ( multipart_get["status"] == 200 and len(multipart_get_body) == expected_multipart_len and sha256_hex(multipart_get_body) == expected_multipart_body_hash ) fallback_rows.append( { "profile": profile, "probe": "multipart", "object_key": multipart_key, "status": "ok" if multipart_ok else "unexpected_status_or_length", "status_code": multipart_get["status"], "body_len": len(multipart_get_body), "expected_runtime_fallback_reason": multipart_expected_reason, "note": multipart_note, } ) if multipart_expected_reason == "none": profile_dir = out_dir.parent data_root = profile_dir / "data" multipart_rel = pathlib.Path(*multipart_key.split("/")) part_candidates = [] for disk_dir in sorted(data_root.glob("disk*")): object_dir = disk_dir / bucket / multipart_rel part_candidates.extend(sorted(object_dir.glob("*/part.1"))) if not part_candidates: fallback_rows.append( { "profile": profile, "probe": "multipart_degraded_read", "object_key": multipart_key, "status": "part_file_missing", "status_code": "N/A", "body_len": 0, "expected_runtime_fallback_reason": "read_quorum_not_safe", "note": "multipart degraded-read probe could not find an on-disk part.1 shard to move", } ) else: multipart_part = part_candidates[0] missing_dir = out_dir / "multipart-degraded-missing-shards" missing_dir.mkdir(parents=True, exist_ok=True) missing_part = missing_dir / f"{multipart_part.parent.name}-{multipart_part.name}" if missing_part.exists(): missing_part.unlink() multipart_part.rename(missing_part) degraded_multipart_get, degraded_multipart_body = request("GET", multipart_path) degraded_multipart_ok = ( degraded_multipart_get["status"] == 200 and len(degraded_multipart_body) == expected_multipart_len and sha256_hex(degraded_multipart_body) == expected_multipart_body_hash ) fallback_rows.append( { "profile": profile, "probe": "multipart_degraded_read", "object_key": multipart_key, "status": "ok" if degraded_multipart_ok else "unexpected_status_or_body", "status_code": degraded_multipart_get["status"], "body_len": len(degraded_multipart_body), "expected_runtime_fallback_reason": "read_quorum_not_safe", "note": f"Moved one multipart shard to {missing_part}; unsafe part setup should fall back before returning a codec reader", } ) else: fallback_rows.append( { "profile": profile, "probe": "multipart_degraded_read", "object_key": multipart_key, "status": "skipped", "status_code": "N/A", "body_len": "N/A", "expected_runtime_fallback_reason": "read_quorum_not_safe", "note": "Enable --codec-multipart on with a multipart max-parts value of at least 2 for degraded-read fallback proof", } ) encrypted_key = object_key + ".encrypted" encrypted_path = bucket_path + "/" + urllib.parse.quote(encrypted_key, safe="/-_.~") ssec_key = b"0123456789abcdef0123456789abcdef" ssec_key_b64 = base64.b64encode(ssec_key).decode() ssec_key_md5_b64 = base64.b64encode(hashlib.md5(ssec_key).digest()).decode() ssec_headers = [ ("x-amz-server-side-encryption-customer-algorithm", "AES256"), ("x-amz-server-side-encryption-customer-key", ssec_key_b64), ("x-amz-server-side-encryption-customer-key-md5", ssec_key_md5_b64), ] encrypted_put, _ = request("PUT", encrypted_path, body, [("content-type", "application/octet-stream"), *ssec_headers]) if encrypted_put["status"] not in (200, 204): fallback_rows.append( { "profile": profile, "probe": "encrypted", "object_key": encrypted_key, "status": "put_failed", "status_code": encrypted_put["status"], "body_len": 0, "expected_runtime_fallback_reason": "encrypted", "note": "SSE-C encrypted object upload failed", } ) else: encrypted_get, encrypted_body = request("GET", encrypted_path, extra_headers=ssec_headers) encrypted_ok = encrypted_get["status"] == 200 and sha256_hex(encrypted_body) == sha256_hex(body) fallback_rows.append( { "profile": profile, "probe": "encrypted", "object_key": encrypted_key, "status": "ok" if encrypted_ok else "unexpected_status_or_body", "status_code": encrypted_get["status"], "body_len": len(encrypted_body), "expected_runtime_fallback_reason": "encrypted", "note": "SSE-C encrypted object GET should stay on the legacy fallback path for codec profiles", } ) body_sha = sha256_hex(get_body) expected_sha = sha256_hex(body) body_match = body_sha == expected_sha headers_report = { "create_bucket": create_bucket, "put_object": put_object, "head_object": head_object, "get_object": get_object, } snapshot = { "profile": profile, "path": profile, "size": object_size, "object_key": object_key, "expected_body_sha256": expected_sha, "body_sha256": body_sha, "body_sha256_match_expected": body_match, "head_status": head_object["status"], "get_status": get_object["status"], "head_headers": header_map(head_object["headers"]), "get_headers": header_map(get_object["headers"]), } (out_dir / "response_headers.json").write_text(json.dumps(headers_report, indent=2, sort_keys=True) + "\n") (out_dir / "snapshot.json").write_text(json.dumps(snapshot, indent=2, sort_keys=True) + "\n") (out_dir / "body_sha256.txt").write_text(body_sha + "\n") with (out_dir / "fallback_probe_summary.csv").open("w", encoding="utf-8", newline="") as handle: fieldnames = [ "profile", "probe", "object_key", "status", "status_code", "body_len", "expected_runtime_fallback_reason", "note", ] writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() writer.writerows(fallback_rows) get_headers = snapshot["get_headers"] content_length = ",".join(get_headers.get("content-length", [])) etag = ",".join(get_headers.get("etag", [])) status_match = head_object["status"] == 200 and get_object["status"] == 200 error_count = 0 if body_match and status_match else 1 (out_dir / "compat_summary.csv").write_text( "size,path,body_sha256_match,content_length_match,etag_match,content_range_match," "checksum_headers_match,sse_headers_match,status_code_match,error_count,body_sha256,status_code,content_length,etag\n" f"{object_size},{profile},{str(body_match).lower()},true,true,true,true,true,{str(status_match).lower()}," f"{error_count},{body_sha},{get_object['status']},{content_length},{etag}\n" ) PY } copy_profile_compat_artifacts() { local profile="$1" local label="$2" local compat_dir="${OUT_DIR}/${profile}/compat" if [[ -f "${compat_dir}/body_sha256.txt" ]]; then cp "${compat_dir}/body_sha256.txt" "${OUT_DIR}/body_sha256_${label}.txt" fi if [[ -f "${compat_dir}/response_headers.json" ]]; then cp "${compat_dir}/response_headers.json" "${OUT_DIR}/response_headers_${label}.json" fi } write_root_compat_summary() { local legacy_snapshot="${OUT_DIR}/legacy/compat/snapshot.json" local codec_snapshots=() local profile snapshot [[ -f "$legacy_snapshot" ]] || return 0 for profile in "$@"; do [[ "$profile" == legacy ]] && continue snapshot="${OUT_DIR}/${profile}/compat/snapshot.json" if [[ -f "$snapshot" ]]; then codec_snapshots+=("$snapshot") fi done [[ "${#codec_snapshots[@]}" -gt 0 ]] || return 0 "$PYTHON_BIN" - "${OUT_DIR}/compat_summary.csv" "$legacy_snapshot" "${codec_snapshots[@]}" <<'PY' import csv import json import sys out_csv = sys.argv[1] legacy_path = sys.argv[2] codec_paths = sys.argv[3:] legacy = json.load(open(legacy_path, encoding="utf-8")) codecs = [json.load(open(path, encoding="utf-8")) for path in codec_paths] def values(snapshot, name): return snapshot.get("get_headers", {}).get(name, []) def prefixed(snapshot, prefix): headers = snapshot.get("get_headers", {}) return {key: headers[key] for key in sorted(headers) if key.startswith(prefix)} with open(out_csv, "w", encoding="utf-8", newline="") as handle: writer = csv.writer(handle) writer.writerow( [ "size", "path", "body_sha256_match", "content_length_match", "etag_match", "content_range_match", "checksum_headers_match", "sse_headers_match", "status_code_match", "error_count", "legacy_body_sha256", "new_body_sha256", "legacy_status_code", "new_status_code", ] ) for codec in codecs: body_match = legacy.get("body_sha256") == codec.get("body_sha256") content_length_match = values(legacy, "content-length") == values(codec, "content-length") etag_match = values(legacy, "etag") == values(codec, "etag") content_range_match = values(legacy, "content-range") == values(codec, "content-range") checksum_headers_match = prefixed(legacy, "x-amz-checksum") == prefixed(codec, "x-amz-checksum") sse_headers_match = prefixed(legacy, "x-amz-server-side-encryption") == prefixed(codec, "x-amz-server-side-encryption") status_code_match = legacy.get("get_status") == codec.get("get_status") and legacy.get("head_status") == codec.get("head_status") checks = [ body_match, content_length_match, etag_match, content_range_match, checksum_headers_match, sse_headers_match, status_code_match, ] writer.writerow( [ codec.get("size", legacy.get("size", "")), codec.get("path", ""), str(body_match).lower(), str(content_length_match).lower(), str(etag_match).lower(), str(content_range_match).lower(), str(checksum_headers_match).lower(), str(sse_headers_match).lower(), str(status_code_match).lower(), sum(1 for check in checks if not check), legacy.get("body_sha256", ""), codec.get("body_sha256", ""), legacy.get("get_status", ""), codec.get("get_status", ""), ] ) PY } write_root_fallback_probe_summary() { local out_csv="${OUT_DIR}/fallback_probe_summary.csv" local profile summary wrote_header=false : >"$out_csv" for profile in "$@"; do summary="${OUT_DIR}/${profile}/compat/fallback_probe_summary.csv" [[ -f "$summary" ]] || continue if [[ "$wrote_header" == "false" ]]; then cat "$summary" >>"$out_csv" wrote_header=true else tail -n +2 "$summary" >>"$out_csv" fi done if [[ "$wrote_header" == "false" ]]; then cat >"$out_csv" <<'EOF' profile,probe,object_key,status,status_code,body_len,expected_runtime_fallback_reason,note N/A,missing,N/A,missing,N/A,N/A,N/A,no_profile_fallback_probe_summary EOF fi } write_root_metrics_summary() { local profile_dirs=() local profile for profile in "$@"; do profile_dirs+=("${OUT_DIR}/${profile}") done [[ "${#profile_dirs[@]}" -gt 0 ]] || return "$PYTHON_BIN" - "${OUT_DIR}/metrics_summary.csv" "${OUT_DIR}/engine_compare.csv" "${OUT_DIR}/baseline_compare.csv" "${profile_dirs[@]}" <<'PY' import csv import pathlib import sys metrics_csv = pathlib.Path(sys.argv[1]) engine_compare_csv = pathlib.Path(sys.argv[2]) baseline_compare_csv = pathlib.Path(sys.argv[3]) profile_dirs = [pathlib.Path(value) for value in sys.argv[4:]] def load_manifest(path): data = {} for line in path.read_text(encoding="utf-8").splitlines(): if "=" not in line: continue key, value = line.split("=", 1) data[key] = value return data def parse_float(value): if value in ("", "N/A", None): return None return float(value) def delta_pct(new_value, baseline_value): if new_value is None or baseline_value in (None, 0.0): return "" return f"{((new_value - baseline_value) / baseline_value) * 100:.2f}" def performance_note(throughput_delta, latency_delta): if throughput_delta == "" or latency_delta == "": return "" throughput_delta = float(throughput_delta) latency_delta = float(latency_delta) if throughput_delta < 0 and latency_delta > 0: return "slower_than_legacy_and_higher_latency" if throughput_delta < 0: return "slower_than_legacy" if latency_delta > 0: return "higher_latency_than_legacy" return "at_or_better_than_legacy" def row_value(row, key): return row.get(key, "") rows = [] by_profile = {} for profile_dir in profile_dirs: manifest = load_manifest(profile_dir / "manifest.env") profile = manifest["profile"] with open(profile_dir / "warp" / "median_summary.csv", encoding="utf-8", newline="") as handle: profile_rows = list(csv.DictReader(handle)) by_profile[profile] = profile_rows for row in profile_rows: rows.append( { "profile": profile, "read_path": manifest.get("metrics_path", profile), "codec_streaming_enabled": manifest.get("RUSTFS_GET_CODEC_STREAMING_ENABLE", ""), "codec_engine": manifest.get("RUSTFS_GET_CODEC_STREAMING_ENGINE", ""), "metadata_early_stop": manifest.get("RUSTFS_GET_METADATA_EARLY_STOP_ENABLE", ""), "shard_locality_preference": manifest.get("RUSTFS_GET_SHARD_LOCALITY_PREFERENCE_ENABLE", ""), "codec_max_inflight": manifest.get("RUSTFS_GET_CODEC_STREAMING_MAX_INFLIGHT", ""), "codec_reader_setup": manifest.get("RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_READER_SETUP", ""), "codec_data_blocks_first_gate": manifest.get("RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_ENABLE", ""), "codec_data_blocks_first_max_size": manifest.get("RUSTFS_GET_CODEC_STREAMING_DATA_BLOCKS_FIRST_MAX_SIZE", ""), "local_read_copy_method": manifest.get("RUSTFS_OBJECT_MMAP_READ_METHOD", ""), "output_handoff_attribution": manifest.get("RUSTFS_GET_OUTPUT_HANDOFF_ATTRIBUTION_ENABLE", ""), "round_cooldown_secs": manifest.get("round_cooldown_secs", ""), "duration": manifest.get("duration", ""), "size": row["size"], "tool": row["tool"], "concurrency": row["concurrency"], "successful_rounds": row["successful_rounds"], "failed_rounds": row["failed_rounds"], "median_throughput_bps": row["median_throughput_bps"], "median_reqps": row["median_reqps"], "median_latency_ms": row["median_latency_ms"], "median_req_p90_ms": row_value(row, "median_req_p90_ms"), "median_req_p99_ms": row_value(row, "median_req_p99_ms"), } ) legacy_by_size = {row["size"]: row for row in by_profile.get("legacy", [])} for row in rows: baseline = legacy_by_size.get(row["size"]) if baseline is None or row["profile"] == "legacy": row["baseline_profile"] = "" row["delta_throughput_pct_vs_legacy"] = "" row["delta_reqps_pct_vs_legacy"] = "" row["delta_latency_pct_vs_legacy"] = "" row["performance_note"] = "" continue throughput_delta = delta_pct(parse_float(row["median_throughput_bps"]), parse_float(baseline["median_throughput_bps"])) reqps_delta = delta_pct(parse_float(row["median_reqps"]), parse_float(baseline["median_reqps"])) latency_delta = delta_pct(parse_float(row["median_latency_ms"]), parse_float(baseline["median_latency_ms"])) p90_delta = delta_pct(parse_float(row.get("median_req_p90_ms", "")), parse_float(baseline.get("median_req_p90_ms", ""))) p99_delta = delta_pct(parse_float(row.get("median_req_p99_ms", "")), parse_float(baseline.get("median_req_p99_ms", ""))) row["baseline_profile"] = "legacy" row["delta_throughput_pct_vs_legacy"] = throughput_delta row["delta_reqps_pct_vs_legacy"] = reqps_delta row["delta_latency_pct_vs_legacy"] = latency_delta row["delta_req_p90_pct_vs_legacy"] = p90_delta row["delta_req_p99_pct_vs_legacy"] = p99_delta row["performance_note"] = performance_note(throughput_delta, latency_delta) fieldnames = [ "profile", "read_path", "codec_streaming_enabled", "codec_engine", "metadata_early_stop", "shard_locality_preference", "codec_max_inflight", "codec_reader_setup", "codec_data_blocks_first_gate", "codec_data_blocks_first_max_size", "local_read_copy_method", "output_handoff_attribution", "round_cooldown_secs", "duration", "size", "tool", "concurrency", "successful_rounds", "failed_rounds", "median_throughput_bps", "median_reqps", "median_latency_ms", "median_req_p90_ms", "median_req_p99_ms", "baseline_profile", "delta_throughput_pct_vs_legacy", "delta_reqps_pct_vs_legacy", "delta_latency_pct_vs_legacy", "delta_req_p90_pct_vs_legacy", "delta_req_p99_pct_vs_legacy", "performance_note", ] with open(metrics_csv, "w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows) baseline_fields = [ "profile", "read_path", "codec_streaming_enabled", "codec_engine", "metadata_early_stop", "shard_locality_preference", "codec_max_inflight", "codec_reader_setup", "codec_data_blocks_first_gate", "codec_data_blocks_first_max_size", "local_read_copy_method", "output_handoff_attribution", "round_cooldown_secs", "duration", "size", "tool", "concurrency", "successful_rounds", "failed_rounds", "baseline_profile", "new_median_reqps", "baseline_median_reqps", "delta_reqps_pct_vs_legacy", "new_median_latency_ms", "baseline_median_latency_ms", "delta_latency_pct_vs_legacy", "new_median_req_p90_ms", "baseline_median_req_p90_ms", "delta_req_p90_pct_vs_legacy", "new_median_req_p99_ms", "baseline_median_req_p99_ms", "delta_req_p99_pct_vs_legacy", "new_median_throughput_bps", "baseline_median_throughput_bps", "delta_throughput_pct_vs_legacy", "performance_note", ] baseline_rows = [] for row in rows: if row["profile"] == "legacy" or row["baseline_profile"] != "legacy": continue baseline = legacy_by_size.get(row["size"]) if baseline is None: continue baseline_rows.append( { "profile": row["profile"], "read_path": row["read_path"], "codec_streaming_enabled": row["codec_streaming_enabled"], "codec_engine": row["codec_engine"], "metadata_early_stop": row["metadata_early_stop"], "shard_locality_preference": row["shard_locality_preference"], "codec_max_inflight": row["codec_max_inflight"], "codec_reader_setup": row["codec_reader_setup"], "codec_data_blocks_first_gate": row["codec_data_blocks_first_gate"], "codec_data_blocks_first_max_size": row["codec_data_blocks_first_max_size"], "local_read_copy_method": row["local_read_copy_method"], "output_handoff_attribution": row["output_handoff_attribution"], "round_cooldown_secs": row["round_cooldown_secs"], "duration": row["duration"], "size": row["size"], "tool": row["tool"], "concurrency": row["concurrency"], "successful_rounds": row["successful_rounds"], "failed_rounds": row["failed_rounds"], "baseline_profile": "legacy", "new_median_reqps": row["median_reqps"], "baseline_median_reqps": baseline.get("median_reqps", ""), "delta_reqps_pct_vs_legacy": row["delta_reqps_pct_vs_legacy"], "new_median_latency_ms": row["median_latency_ms"], "baseline_median_latency_ms": baseline.get("median_latency_ms", ""), "delta_latency_pct_vs_legacy": row["delta_latency_pct_vs_legacy"], "new_median_req_p90_ms": row.get("median_req_p90_ms", ""), "baseline_median_req_p90_ms": baseline.get("median_req_p90_ms", ""), "delta_req_p90_pct_vs_legacy": row.get("delta_req_p90_pct_vs_legacy", ""), "new_median_req_p99_ms": row.get("median_req_p99_ms", ""), "baseline_median_req_p99_ms": baseline.get("median_req_p99_ms", ""), "delta_req_p99_pct_vs_legacy": row.get("delta_req_p99_pct_vs_legacy", ""), "new_median_throughput_bps": row["median_throughput_bps"], "baseline_median_throughput_bps": baseline.get("median_throughput_bps", ""), "delta_throughput_pct_vs_legacy": row["delta_throughput_pct_vs_legacy"], "performance_note": row["performance_note"], } ) with open(baseline_compare_csv, "w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=baseline_fields) writer.writeheader() writer.writerows(baseline_rows) codec_legacy_by_size = {row["size"]: row for row in by_profile.get("codec-legacy", [])} codec_rustfs_by_size = {row["size"]: row for row in by_profile.get("codec-rustfs", [])} if not legacy_by_size: raise SystemExit(0) compare_fields = [ "size", "legacy_median_throughput_bps", "codec_legacy_median_throughput_bps", "codec_rustfs_median_throughput_bps", "legacy_median_reqps", "codec_legacy_median_reqps", "codec_rustfs_median_reqps", "legacy_median_latency_ms", "codec_legacy_median_latency_ms", "codec_rustfs_median_latency_ms", "legacy_median_req_p90_ms", "codec_legacy_median_req_p90_ms", "codec_rustfs_median_req_p90_ms", "legacy_median_req_p99_ms", "codec_legacy_median_req_p99_ms", "codec_rustfs_median_req_p99_ms", "codec_legacy_delta_throughput_pct_vs_legacy", "codec_rustfs_delta_throughput_pct_vs_legacy", "codec_rustfs_delta_throughput_pct_vs_codec_legacy", "codec_legacy_delta_latency_pct_vs_legacy", "codec_rustfs_delta_latency_pct_vs_legacy", "codec_rustfs_delta_latency_pct_vs_codec_legacy", "codec_legacy_delta_req_p90_pct_vs_legacy", "codec_rustfs_delta_req_p90_pct_vs_legacy", "codec_rustfs_delta_req_p90_pct_vs_codec_legacy", "codec_legacy_delta_req_p99_pct_vs_legacy", "codec_rustfs_delta_req_p99_pct_vs_legacy", "codec_rustfs_delta_req_p99_pct_vs_codec_legacy", ] compare_rows = [] for size, legacy in legacy_by_size.items(): codec_legacy = codec_legacy_by_size.get(size, {}) codec_rustfs = codec_rustfs_by_size.get(size, {}) compare_rows.append( { "size": size, "legacy_median_throughput_bps": legacy.get("median_throughput_bps", ""), "codec_legacy_median_throughput_bps": codec_legacy.get("median_throughput_bps", ""), "codec_rustfs_median_throughput_bps": codec_rustfs.get("median_throughput_bps", ""), "legacy_median_reqps": legacy.get("median_reqps", ""), "codec_legacy_median_reqps": codec_legacy.get("median_reqps", ""), "codec_rustfs_median_reqps": codec_rustfs.get("median_reqps", ""), "legacy_median_latency_ms": legacy.get("median_latency_ms", ""), "codec_legacy_median_latency_ms": codec_legacy.get("median_latency_ms", ""), "codec_rustfs_median_latency_ms": codec_rustfs.get("median_latency_ms", ""), "legacy_median_req_p90_ms": legacy.get("median_req_p90_ms", ""), "codec_legacy_median_req_p90_ms": codec_legacy.get("median_req_p90_ms", ""), "codec_rustfs_median_req_p90_ms": codec_rustfs.get("median_req_p90_ms", ""), "legacy_median_req_p99_ms": legacy.get("median_req_p99_ms", ""), "codec_legacy_median_req_p99_ms": codec_legacy.get("median_req_p99_ms", ""), "codec_rustfs_median_req_p99_ms": codec_rustfs.get("median_req_p99_ms", ""), "codec_legacy_delta_throughput_pct_vs_legacy": delta_pct( parse_float(codec_legacy.get("median_throughput_bps", "")), parse_float(legacy.get("median_throughput_bps", "")), ), "codec_rustfs_delta_throughput_pct_vs_legacy": delta_pct( parse_float(codec_rustfs.get("median_throughput_bps", "")), parse_float(legacy.get("median_throughput_bps", "")), ), "codec_rustfs_delta_throughput_pct_vs_codec_legacy": delta_pct( parse_float(codec_rustfs.get("median_throughput_bps", "")), parse_float(codec_legacy.get("median_throughput_bps", "")), ), "codec_legacy_delta_latency_pct_vs_legacy": delta_pct( parse_float(codec_legacy.get("median_latency_ms", "")), parse_float(legacy.get("median_latency_ms", "")), ), "codec_rustfs_delta_latency_pct_vs_legacy": delta_pct( parse_float(codec_rustfs.get("median_latency_ms", "")), parse_float(legacy.get("median_latency_ms", "")), ), "codec_rustfs_delta_latency_pct_vs_codec_legacy": delta_pct( parse_float(codec_rustfs.get("median_latency_ms", "")), parse_float(codec_legacy.get("median_latency_ms", "")), ), "codec_legacy_delta_req_p90_pct_vs_legacy": delta_pct( parse_float(codec_legacy.get("median_req_p90_ms", "")), parse_float(legacy.get("median_req_p90_ms", "")), ), "codec_rustfs_delta_req_p90_pct_vs_legacy": delta_pct( parse_float(codec_rustfs.get("median_req_p90_ms", "")), parse_float(legacy.get("median_req_p90_ms", "")), ), "codec_rustfs_delta_req_p90_pct_vs_codec_legacy": delta_pct( parse_float(codec_rustfs.get("median_req_p90_ms", "")), parse_float(codec_legacy.get("median_req_p90_ms", "")), ), "codec_legacy_delta_req_p99_pct_vs_legacy": delta_pct( parse_float(codec_legacy.get("median_req_p99_ms", "")), parse_float(legacy.get("median_req_p99_ms", "")), ), "codec_rustfs_delta_req_p99_pct_vs_legacy": delta_pct( parse_float(codec_rustfs.get("median_req_p99_ms", "")), parse_float(legacy.get("median_req_p99_ms", "")), ), "codec_rustfs_delta_req_p99_pct_vs_codec_legacy": delta_pct( parse_float(codec_rustfs.get("median_req_p99_ms", "")), parse_float(codec_legacy.get("median_req_p99_ms", "")), ), } ) with open(engine_compare_csv, "w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=compare_fields) writer.writeheader() writer.writerows(compare_rows) PY } write_root_cpu_rss_notes() { local notes_file="${OUT_DIR}/cpu_rss_notes.txt" : >"$notes_file" for profile_dir in "${OUT_DIR}"/*; do [[ -d "$profile_dir" ]] || continue if [[ -f "${profile_dir}/cpu_rss_notes.txt" ]]; then { cat "${profile_dir}/cpu_rss_notes.txt" printf '\n' } >>"$notes_file" fi done } write_raw_output_paths() { local out_file="${OUT_DIR}/raw_output_paths.txt" { find "$OUT_DIR" -maxdepth 1 -type f \ \( -name 'metrics_summary.csv' \ -o -name 'service_metrics_summary.csv' \ -o -name 'service_metrics_round_summary.csv' \ -o -name 'service_metrics_stage_distribution.csv' \ -o -name 'service_metrics_round_percentiles.csv' \ -o -name 'service_metrics_acceptance.csv' \) | sort for profile_dir in "${OUT_DIR}"/*; do [[ -d "$profile_dir" ]] || continue find "$profile_dir" -maxdepth 1 -type f \ \( -name 'metrics_summary.csv' \ -o -name 'service_metrics_summary.csv' \ -o -name 'service_metrics_round_summary.csv' \ -o -name 'service_metrics_stage_distribution.csv' \ -o -name 'service_metrics_round_percentiles.csv' \) | sort if [[ -d "${profile_dir}/service-metrics" ]]; then find "${profile_dir}/service-metrics" -type f | sort fi if [[ -d "${profile_dir}/warp/logs" ]]; then find "${profile_dir}/warp/logs" -type f | sort fi if [[ -d "${profile_dir}/warp_prepare" ]]; then find "${profile_dir}/warp_prepare" -type f | sort fi done } >"$out_file" } write_default_switch_readiness_report() { local report_path="${OUT_DIR}/default_switch_readiness.md" local baseline_compare_path="${OUT_DIR}/baseline_compare.csv" local compat_summary_path="${OUT_DIR}/compat_summary.csv" local cpu_rss_notes_path="${OUT_DIR}/cpu_rss_notes.txt" local fallback_coverage_path="${OUT_DIR}/fallback_coverage.txt" local service_metrics_acceptance_path="${OUT_DIR}/service_metrics_acceptance.csv" local tracking_issues_path="${OUT_DIR}/tracking_issues.txt" local stable not_worse improved_over_five two_sizes_gt_five local headers_compatible p95_p99_ok cpu_rss_ok runtime_metrics_ok fallback_proven kill_switch_verified correctness_matrix_ok local decision if [[ -f "$baseline_compare_path" ]]; then read -r stable not_worse improved_over_five < <( awk -F',' -v expected_rounds="$ROUNDS" ' BEGIN { rows = 0 stable = 1 not_worse = 1 improved = 0 } NR == 1 { next } $11 == "1MiB" || $11 == "4MiB" || $11 == "10MiB" { rows++ if ($14 != expected_rounds || $15 != "0") { stable = 0 } if ($19 == "" || $22 == "" || ($19 + 0.0) < 0.0 || ($22 + 0.0) > 0.0) { not_worse = 0 } if ($31 != "" && ($31 + 0.0) > 5.0) { improved++ } } END { if (rows == 0) { stable = 0 not_worse = 0 } printf "%s %s %d\n", stable ? "pass" : "fail", not_worse ? "pass" : "fail", improved } ' "$baseline_compare_path" ) else stable="fail" not_worse="fail" improved_over_five=0 fi if [[ "$improved_over_five" -ge 2 ]]; then two_sizes_gt_five="pass" else two_sizes_gt_five="fail" fi if [[ -f "$compat_summary_path" ]] && awk -F',' 'NR > 1 && $10 != "0" { exit 1 } END { exit (NR > 1 ? 0 : 1) }' "$compat_summary_path"; then headers_compatible="pass" else headers_compatible="fail" fi p95_p99_ok="fail" correctness_matrix_ok="fail" runtime_metrics_ok="fail" if [[ "$DIAGNOSTIC_METRICS" == "true" && -f "$service_metrics_acceptance_path" ]] \ && "$PYTHON_BIN" - "$service_metrics_acceptance_path" <<'PY' import csv import sys with open(sys.argv[1], encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle)) statuses = [row.get("status") for row in rows] if not rows or "fail" in statuses or "pass" not in statuses: raise SystemExit(1) PY then runtime_metrics_ok="pass" elif [[ "$DIAGNOSTIC_METRICS" != "true" ]]; then runtime_metrics_ok="skipped" fi if [[ -f "$cpu_rss_notes_path" ]] && grep -q 'cpu_rss_acceptability=acceptable' "$cpu_rss_notes_path"; then cpu_rss_ok="pass" else cpu_rss_ok="fail" fi if [[ -f "$fallback_coverage_path" ]] \ && grep -q 'read_quorum_not_safe' "$fallback_coverage_path" \ && grep -q 'range / encrypted / compressed / remote' "$fallback_coverage_path"; then fallback_proven="pass" else fallback_proven="fail" fi if [[ -f "$fallback_coverage_path" ]] && grep -q 'codec_streaming_reader_gate_defaults_to_disabled' "$fallback_coverage_path"; then kill_switch_verified="pass" else kill_switch_verified="fail" fi if [[ "$stable" == "pass" \ && "$not_worse" == "pass" \ && "$two_sizes_gt_five" == "pass" \ && "$p95_p99_ok" == "pass" \ && "$cpu_rss_ok" == "pass" \ && "$runtime_metrics_ok" == "pass" \ && "$correctness_matrix_ok" == "pass" \ && "$headers_compatible" == "pass" \ && "$fallback_proven" == "pass" \ && "$kill_switch_verified" == "pass" ]]; then decision="Default enablement prerequisites passed; scoped default enablement may be considered." else decision="Default enablement is not ready; keep opt-in only." fi { echo "# GET V2 PR-35 Default Switch Readiness" echo echo "Decision: **${decision}**" echo echo "## Hard Prerequisite Status" echo echo "| Prerequisite | Status | Notes |" echo "| --- | --- | --- |" echo "| multi-round cooled A/B stable | ${stable} | expected rounds per target size: ${ROUNDS} |" echo "| 1MiB / 4MiB / 10MiB not worse than legacy | ${not_worse} | derived from baseline_compare.csv |" echo "| at least two sizes improve by more than 5% | ${two_sizes_gt_five} | qualifying sizes: ${improved_over_five} |" echo "| p95 / p99 do not regress | ${p95_p99_ok} | p99 is aggregated in metrics_summary.csv; p95 remains unavailable in current warp text output |" echo "| CPU and RSS acceptable | ${cpu_rss_ok} | see cpu_rss_notes.txt |" echo "| runtime server metrics prove path and fallback deltas | ${runtime_metrics_ok} | see service_metrics_acceptance.csv; skipped for observability-off performance runs |" echo "| correctness matrix passes | ${correctness_matrix_ok} | compat smoke passes, but full correctness matrix is not fully evidenced by this harness alone |" echo "| response headers compatible | ${headers_compatible} | see compat_summary.csv |" echo "| range / encrypted / compressed / remote fallback proven | ${fallback_proven} | see fallback_coverage.txt |" echo "| kill switch verified | ${kill_switch_verified} | see fallback_coverage.txt |" echo echo "## Evidence Files" echo echo "- \`baseline_compare.csv\`" echo "- \`compat_summary.csv\`" echo "- \`metrics_summary.csv\`" echo "- \`service_metrics_summary.csv\`" echo "- \`service_metrics_acceptance.csv\`" echo "- \`fallback_probe_summary.csv\`" echo "- \`cpu_rss_notes.txt\`" echo "- \`fallback_coverage.txt\`" echo "- \`tracking_issues.txt\`" echo "- \`raw_output_paths.txt\`" echo echo "## Tracking Issues" echo echo '```text' if [[ -f "$tracking_issues_path" ]]; then cat "$tracking_issues_path" fi echo '```' } >"$report_path" } run_profile() { local profile="$1" local baseline_csv="${2:-}" local bench_rc=0 stop_server start_server "$profile" ensure_benchmark_buckets_exist "$profile" prepare_warp_existing_objects "$profile" warmup_warp_get_before_bench "$profile" capture_service_metrics_snapshot "$profile" before if run_bench "$profile" "$baseline_csv"; then : else bench_rc=$? fi write_metrics_summary "$profile" if [[ "$SKIP_COMPAT_PROBE" == "true" ]]; then log "Skipping compatibility probe for profile=${profile}" write_skipped_compat_probe "$profile" else run_compat_probe "$profile" fi if [[ "$DIAGNOSTIC_METRICS" == "true" && "$DIAGNOSTIC_METRICS_SETTLE_SECS" -gt 0 ]]; then sleep "$DIAGNOSTIC_METRICS_SETTLE_SECS" fi capture_service_metrics_snapshot "$profile" after write_profile_service_metrics_summary "$profile" write_profile_service_metrics_round_breakdown "$profile" stop_server write_profile_cpu_rss_notes "$profile" log "Median summary: ${OUT_DIR}/${profile}/warp/median_summary.csv" log "Metrics summary: ${OUT_DIR}/${profile}/metrics_summary.csv" log "Service metrics summary: ${OUT_DIR}/${profile}/service_metrics_summary.csv" log "Service metrics round summary: ${OUT_DIR}/${profile}/service_metrics_round_summary.csv" log "Service metrics stage distribution: ${OUT_DIR}/${profile}/service_metrics_stage_distribution.csv" log "Service metrics round percentiles: ${OUT_DIR}/${profile}/service_metrics_round_percentiles.csv" log "Compatibility summary: ${OUT_DIR}/${profile}/compat/compat_summary.csv" if [[ -f "${OUT_DIR}/${profile}/warp/baseline_compare.csv" ]]; then log "Baseline compare: ${OUT_DIR}/${profile}/warp/baseline_compare.csv" fi return "$bench_rc" } main() { ORIGINAL_ARGS=("$@") parse_args "$@" validate_args setup_output run_detached_if_requested local codec_profiles=() local profiles=() local profiles_csv="" local raw engine profile IFS=',' read -r -a engines <<< "$CODEC_ENGINES" for raw in "${engines[@]}"; do engine="${raw//[[:space:]]/}" [[ -n "$engine" ]] || continue profile="codec-${engine}" codec_profiles+=("$profile") done case "$MODE" in legacy) profiles=("legacy") ;; codec) profiles=("${codec_profiles[@]}") ;; both) profiles=("legacy" "${codec_profiles[@]}") ;; esac if [[ "$PROFILE_ORDER" == "reverse" ]]; then local reversed_profiles=() local profile_index for ((profile_index=${#profiles[@]} - 1; profile_index >= 0; profile_index--)); do reversed_profiles+=("${profiles[$profile_index]}") done profiles=("${reversed_profiles[@]}") fi profiles_csv="$(IFS=,; echo "${profiles[*]}")" write_root_environment write_root_manifest "$profiles_csv" build_rustfs_if_needed trap stop_server EXIT INT TERM log "Output dir: $OUT_DIR" local legacy_baseline_csv="" for profile in "${profiles[@]}"; do if [[ "$profile" == "legacy" ]]; then if ! run_profile "$profile"; then PROFILE_FAILURES=$((PROFILE_FAILURES + 1)) fi if [[ -f "${OUT_DIR}/legacy/warp/median_summary.csv" ]]; then legacy_baseline_csv="${OUT_DIR}/legacy/warp/median_summary.csv" fi else if ! run_profile "$profile" "$legacy_baseline_csv"; then PROFILE_FAILURES=$((PROFILE_FAILURES + 1)) fi fi copy_profile_compat_artifacts "$profile" "${profile//-/_}" done write_root_metrics_summary "${profiles[@]}" write_root_service_metrics_summary "${profiles[@]}" write_root_profile_csv service_metrics_round_summary.csv "${profiles[@]}" write_root_profile_csv service_metrics_stage_distribution.csv "${profiles[@]}" write_root_profile_csv service_metrics_round_percentiles.csv "${profiles[@]}" write_service_metrics_acceptance write_root_compat_summary "${profiles[@]}" write_root_fallback_probe_summary "${profiles[@]}" write_root_cpu_rss_notes write_fallback_coverage write_tracking_issues write_raw_output_paths write_default_switch_readiness_report if [[ -f "${OUT_DIR}/compat_summary.csv" ]]; then log "Compatibility compare: ${OUT_DIR}/compat_summary.csv" fi if [[ -f "${OUT_DIR}/engine_compare.csv" ]]; then log "Engine compare: ${OUT_DIR}/engine_compare.csv" fi if [[ -f "${OUT_DIR}/metrics_summary.csv" ]]; then log "Metrics summary: ${OUT_DIR}/metrics_summary.csv" fi if [[ -f "${OUT_DIR}/service_metrics_summary.csv" ]]; then log "Service metrics summary: ${OUT_DIR}/service_metrics_summary.csv" fi if [[ -f "${OUT_DIR}/service_metrics_acceptance.csv" ]]; then log "Service metrics acceptance: ${OUT_DIR}/service_metrics_acceptance.csv" fi if [[ -f "${OUT_DIR}/fallback_probe_summary.csv" ]]; then log "Fallback probe summary: ${OUT_DIR}/fallback_probe_summary.csv" fi if [[ -f "${OUT_DIR}/baseline_compare.csv" ]]; then log "Baseline compare: ${OUT_DIR}/baseline_compare.csv" fi if [[ -f "${OUT_DIR}/default_switch_readiness.md" ]]; then log "Default switch readiness: ${OUT_DIR}/default_switch_readiness.md" fi if [[ "$DRY_RUN" != "true" && "$PROFILE_FAILURES" -gt 0 ]]; then die "one or more benchmark profiles reported failed rounds; see per-profile median/baseline outputs for details" fi log "GET codec streaming smoke finished." } main "$@"