Compare commits

..
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 cf550b69fd WIP (session wrap-up 2026-10-15): unfinished, not reviewed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 12:36:19 +02:00
1284 changed files with 17611 additions and 256816 deletions

No files matched your search

-17
View File
@@ -1,17 +0,0 @@
# Changes to these files wait for Ludovic's review once branch protection requires code owners' reviews
# (tools/ci/protect_pipeline.sh, docs/security.md). They are what builds, signs and ships releases, the code that
# checks downloads before installing them (backend.rs downloads, installs and updates the launcher itself; links.json
# says where from; the Cargo.toml files pick the crates that do it), and this file. Everything else merges as soon as
# `verify` passes.
/.github/ @ludovic111
/tools/ci/ @ludovic111
/tools/release_train.py @ludovic111
/tools/release_channels.py @ludovic111
/crates/horizon-release-sign/ @ludovic111
/crates/horizon-server/src/update.rs @ludovic111
/crates/horizon-server/Cargo.toml @ludovic111
/launcher/src/signed.rs @ludovic111
/launcher/src/backend.rs @ludovic111
/launcher/links.json @ludovic111
/launcher/Cargo.toml @ludovic111
/packaging/ @ludovic111
-1
View File
@@ -1 +0,0 @@
0.2
-27
View File
@@ -1,27 +0,0 @@
name: cancel-on-close
# A closed or merged pull request's queued and running checks free the runners at once.
# pull_request_target so the token can cancel runs of pull requests from forks too;
# it checks out nothing and runs no code from the pull request.
on:
pull_request_target:
types: [closed]
permissions:
actions: write
jobs:
cancel:
runs-on: ubuntu-latest
steps:
- name: Cancel the pull request's unfinished runs
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.event.pull_request.head.ref }}
SHA: ${{ github.event.pull_request.head.sha }}
run: |
for status in queued in_progress waiting pending requested; do
gh run list -R "$REPO" --branch "$BRANCH" --status "$status" --limit 100 \
--json databaseId,event,headSha \
-q ".[] | select(.event == \"pull_request\" and .headSha == \"$SHA\") | .databaseId"
done | sort -u | while read -r id; do
gh run cancel -R "$REPO" "$id" || true
done
+39 -31
View File
@@ -1,29 +1,23 @@
name: checks
# verify: every pull request and every push to main, on GitHub's runners (the repository is public). It is the
# check main requires, so it never has a condition: a skipped job counts as passed, and every run of this workflow
# must report a real verify (a pull request that changes no Rust finishes it in seconds instead).
# verify: every pull request and every push to main, on GitHub's runners (the repository is public).
# parity: the vanilla harnesses a pull request's changes can affect, on the Mac mini's runner, which holds the
# maintainer's own Minecraft files (Horizon never downloads the game); .github/workflows/parity.yml. A fork's pull
# request runs it only when a maintainer adds the run-parity label (.github/workflows/run-parity.yml), so labels
# never start this workflow. See docs/release-flow.md.
# maintainer's own Minecraft files (Horizon never downloads the game). A fork's pull request runs it only when
# a maintainer adds the run-parity label, once per label. See docs/release-flow.md.
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
permissions:
contents: read
concurrency:
# A new push to a pull request replaces its running checks; main's runs all finish (releases follow them).
group: checks-${{ github.event.pull_request.number || github.sha }}
group: checks-${{ github.event.pull_request.number || github.sha }}-${{ github.event.action == 'labeled' && 'label' || 'run' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
verify:
if: github.event.action != 'labeled'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -33,13 +27,6 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Does this pull request need Rust checks?
if: github.event_name == 'pull_request'
id: rust
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 tools/ci/rust_changes.py "$BASE_SHA" "$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Keep game artifacts out of Git
run: |
python tools/check_public_tree.py
@@ -47,32 +34,53 @@ jobs:
- name: Test offline research tooling
run: python -m unittest discover -s tests -v
- name: Install Rust toolchain and the ALSA and udev headers the client links against
if: steps.rust.outputs.required != 'false'
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
rustup toolchain install stable --profile minimal --component rustfmt --component clippy --target wasm32-unknown-unknown
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
# Dependencies and the workspace's own build, saved from main's runs (pull requests read main's cache).
- uses: Swatinem/rust-cache@v2
if: steps.rust.outputs.required != 'false'
with:
shared-key: verify
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check formatting and lint
if: steps.rust.outputs.required != 'false'
run: |
cargo +stable fmt --all -- --check
cargo +stable clippy --workspace --all-targets -- -D warnings
# The server build (released as horizon-engine): the game without sound or controllers.
cargo +stable clippy -p horizon-client --all-targets --no-default-features -- -D warnings
- name: Test Rust core and voxel kernel correctness
if: steps.rust.outputs.required != 'false'
run: cargo +stable test --workspace
parity:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/parity.yml
with:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
if: >-
github.event_name == 'pull_request' && (
(github.event.pull_request.head.repo.full_name == github.repository && github.event.action != 'labeled') ||
(github.event.action == 'labeled' && github.event.label.name == 'run-parity'))
runs-on: [self-hosted, horizon-game]
timeout-minutes: 120 # a change to core crates reruns ~300 harnesses: ~60-90 min on the Mac mini
steps:
- uses: actions/checkout@v4
with:
# The commit the event names: a fork's later pushes need another run-parity label.
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
persist-credentials: false
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
clean: false
- uses: dtolnay/rust-toolchain@stable
- name: Tools from the runner
run: bash tools/ci/runner-env.sh
- name: Vanilla data, and resources prepared by this preparer
run: bash tools/ci/vanilla-data.sh
- name: The harnesses this change can affect
env:
PARITY_HOSTS: ''
run: python3 tools/parity.py --changed ${{ github.event.pull_request.base.sha }} --timeout 1500
- name: Logs of the failed harnesses
if: failure()
uses: actions/upload-artifact@v4
with:
name: parity-logs
path: .local/parity-runner/logs/
if-no-files-found: ignore
retention-days: 3
# Horizon never downloads the game. These jobs need a self-hosted Linux runner
# (label horizon-game) holding the maintainer's own copy: the official launcher's
# game directory in HORIZON_MINECRAFT_DIR and the pinned server jar in
+16 -95
View File
@@ -1,16 +1,11 @@
name: launcher
# Builds the launcher on Linux, macOS and Windows, and releases it by itself (docs/release-flow.md): after a
# push to main that changes the launcher or a crate it builds in (the paths below),
# push to main that changes the launcher or what it builds in (crates/horizon-prepare, crates/horizon-theme),
# every 10 minutes (a release waiting for merges to settle goes out) and by hand, tools/release_train.py decides as
# for the game.
# A release is launcher@X.Y.Z on the release line in .github/release-line.txt (docs/versioning.md); without it
# launcher-vX.Y.Z, the patch after the last launcher tag or the next minor when a merged pull request is labelled
# feature. The version and the tag are given to the build (HORIZON_LAUNCHER_VERSION, HORIZON_LAUNCHER_TAG): no bump
# commits.
# A release is launcher-vX.Y.Z: the patch after the last launcher tag, or the next minor when a merged pull
# request is labelled feature. The version is given to the build (HORIZON_LAUNCHER_VERSION): no bump commits.
# Never marked latest. Pushes to launcher branches only build. RELEASES_PAUSED=1 stops releases.
# macOS: with the repository variable MACOS_SIGN_IDENTITY set, Horizon.app and the .dmg are signed with
# Developer ID, notarized and stapled, from the runner's signing keychain (packaging/macos/README.md);
# unset (forks), they are signed ad hoc.
# gh workflow run launcher --ref <branch> # dry run: builds everything, publishes nothing
# gh workflow run launcher --ref main -f dry_run=false -f force=true
@@ -18,15 +13,8 @@ on:
push:
branches: [main, launcher, 'launcher-*']
paths:
# Every crate the launcher builds in, as tools/release_train.py's LAUNCHER_PATHS.
- 'launcher/**'
- 'crates/horizon-bench/**'
- 'crates/horizon-friends/**'
- 'crates/horizon-mods/**'
- 'crates/horizon-port-pack/**'
- 'crates/horizon-prepare/**'
- 'crates/horizon-release-sign/**'
- 'crates/horizon-telemetry/**'
- 'crates/horizon-theme/**'
- '.github/workflows/launcher.yml'
schedule:
@@ -49,10 +37,6 @@ concurrency:
group: launcher-${{ github.ref == 'refs/heads/main' && 'release' || github.ref }}
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
decide:
runs-on: ubuntu-latest
@@ -81,8 +65,7 @@ jobs:
python3 tools/release_train.py decide --kind launcher ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
tag=$(jq -r '.tag' decision.json)
version=${tag#launcher-v}
echo "version=${version#launcher@}" >> "$GITHUB_OUTPUT"
echo "version=${tag#launcher-v}" >> "$GITHUB_OUTPUT"
# A dry run (and a launcher branch's push) builds whatever the decision.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then
echo "release=true" >> "$GITHUB_OUTPUT"
@@ -113,20 +96,15 @@ jobs:
# which only the Visual C++ redistributable installs.
rustflags: -C target-feature=+crt-static
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
# Notarizing the app and then the .dmg can take a few minutes each.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
timeout-minutes: 40
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# Horizon's Azure app for Microsoft sign-in (docs/microsoft-account.md): a repository
# variable, not a secret. Unset, the launcher says sign-in is not configured.
HORIZON_MSA_CLIENT_ID: ${{ vars.HORIZON_MSA_CLIENT_ID }}
# The mod browser's CurseForge source (docs/mod-browser.md#curseforge): a repository secret. Unset, the
# launcher searches Modrinth only.
HORIZON_CURSEFORGE_API_KEY: ${{ secrets.HORIZON_CURSEFORGE_API_KEY }}
# The release's version and tag, built in (launcher/src/backend.rs VERSION and own_tag: the tag says which
# line it is on, docs/versioning.md); a launcher branch's build keeps Cargo.toml's.
# The release's version, built in (launcher/src/backend.rs VERSION); a launcher branch's build keeps
# Cargo.toml's.
HORIZON_LAUNCHER_VERSION: ${{ (github.event_name != 'push' || github.ref == 'refs/heads/main') && needs.decide.outputs.version || '' }}
HORIZON_LAUNCHER_TAG: ${{ (github.event_name != 'push' || github.ref == 'refs/heads/main') && needs.decide.outputs.tag || '' }}
defaults:
run:
working-directory: launcher
@@ -136,9 +114,8 @@ jobs:
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave may end up in a launcher release (target/ is rebuilt from scratch).
clean: true
# The Mac mini's runner keeps its target/ between runs.
clean: ${{ runner.environment == 'github-hosted' }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
@@ -159,8 +136,7 @@ jobs:
echo "::notice::No Metal toolchain on this runner: GPUI compiles its shaders at run time"
echo "LAUNCHER_FEATURES=gpui/runtime_shaders" >> "$GITHUB_ENV"
fi
# Every test target (the library, the binary, tests/), on each system the launcher ships for.
- run: cargo test --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- run: cargo test --release --locked --lib ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- run: cargo build --release --locked ${LAUNCHER_FEATURES:+--features "$LAUNCHER_FEATURES"}
- name: The version is the release's
run: |
@@ -192,6 +168,9 @@ jobs:
retry "$launcher" install latest
"$launcher" versions --installed
"$launcher" links --json
"$launcher" menu-style classic
grep -qx 'menuStyle:classic' "$HORIZON_HOME/horizon.txt"
"$launcher" menu-style horizon
clients=("$HORIZON_LAUNCHER_HOME"/versions/v*/horizon-client*)
client="${clients[0]}"
echo "installed client: $client"
@@ -207,19 +186,11 @@ jobs:
help() { "$client" --help > "$RUNNER_TEMP/help.txt" 2>&1 && head -3 "$RUNNER_TEMP/help.txt" && grep -q -- '--resources' "$RUNNER_TEMP/help.txt"; }
soft help
rm -rf "$HORIZON_LAUNCHER_HOME"
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: ../packaging/macos/keychain.sh unlock
- name: Package
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
rm -rf dist out && mkdir -p out
if [[ "$RUNNER_OS" == "macOS" ]]; then
# Horizon.app, zipped (self-update) and in a .dmg (installer), signed (and notarized with
# Developer ID): packaging/macos.
# Horizon.app, zipped (self-update) and in a .dmg (installer): packaging/macos.
python3 -m venv "$RUNNER_TEMP/dmg" && "$RUNNER_TEMP/dmg/bin/pip" install --quiet dmgbuild==1.6.5
version=${HORIZON_LAUNCHER_VERSION:-$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)}
DMGBUILD="$RUNNER_TEMP/dmg/bin/dmgbuild" ../packaging/macos/package.sh target/release/${{ matrix.binary }} "$version" out
@@ -239,53 +210,11 @@ jobs:
name: ${{ matrix.asset }}
path: launcher/out/*
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: ../packaging/macos/keychain.sh lock
sign:
release:
needs: [decide, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-launcher-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: bash tools/ci/sign_release.sh "$TAG" dist/horizon-launcher-*
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
release:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
@@ -303,13 +232,6 @@ jobs:
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
# Never marked latest: Horizon's own releases stay the repository's latest.
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
@@ -317,9 +239,8 @@ jobs:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --latest=false dist/horizon-launcher-* dist/SHA256SUMS "${signature[@]}"
--notes-file notes/body.md --latest=false dist/horizon-launcher-*
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
-4
View File
@@ -23,10 +23,6 @@ permissions:
concurrency:
group: nightly-audit
cancel-in-progress: false
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
audit:
runs-on: [self-hosted, horizon-game]
-54
View File
@@ -1,54 +0,0 @@
name: parity
# The vanilla harnesses a pull request's changes can affect, on the Mac mini's runner, which holds the maintainer's
# own Minecraft files (Horizon never downloads the game). Called by ci.yml for pull requests from this repository
# and by run-parity.yml when a maintainer labels a fork's pull request run-parity. See docs/release-flow.md.
on:
workflow_call:
inputs:
base:
description: The pull request's base commit (the harnesses its changes since then can affect)
type: string
required: true
head:
description: The commit to check out and test
type: string
required: true
permissions:
contents: read
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity:
runs-on: [self-hosted, horizon-game]
timeout-minutes: 120 # a change to core crates reruns ~300 harnesses: ~60-90 min on the Mac mini
steps:
- uses: actions/checkout@v4
with:
# The commit the event names: a fork's later pushes need another run-parity label.
ref: ${{ inputs.head }}
fetch-depth: 0
persist-credentials: false
# Keeps .local/ (parity state, probe builds, oracle results) and target/ between runs.
clean: false
- uses: dtolnay/rust-toolchain@stable
- name: Tools from the runner
run: bash tools/ci/runner-env.sh
- name: Vanilla data, and resources prepared by this preparer
run: bash tools/ci/vanilla-data.sh
- name: The version constants are the pinned jar's
run: python3 tools/gen_version_consts.py --check
- name: The harnesses this change can affect
env:
PARITY_HOSTS: ''
BASE_SHA: ${{ inputs.base }}
run: python3 tools/parity.py --changed "$BASE_SHA" --timeout 2400 # world_spawn takes ~1480 s on a busy runner
- name: Logs of the failed harnesses
if: failure()
uses: actions/upload-artifact@v4
with:
name: parity-logs
path: .local/parity-runner/logs/
if-no-files-found: ignore
retention-days: 3
-55
View File
@@ -1,55 +0,0 @@
name: promote
# Beta game releases become stable once a day (docs/release-flow.md, "Channels"): at 20:00 UTC (21:00 in Paris in
# winter time, 22:00 in summer) tools/release_channels.py promotes the newest beta that had been out 6 hours by then,
# passed its release smoke test and isn't covered by an issue labelled release-blocker. Launchers on the Alpha
# channel install every beta when it is published; Stable gets the day's promotion. The later runs of the evening are
# retries for a run GitHub starts late or skips: they promote the same beta, so stable still moves once.
# By hand, from GitHub's app too (Actions > promote > Run workflow):
# gh workflow run promote -f tag=v0.2.3 # that beta now, without waiting for 20:00
# gh workflow run promote -f tag=v0.2.3 -f force=true # even blocked or without its smoke test
# gh workflow run promote -f dry_run=true # say what would be promoted
# Holding betas back: label an issue release-blocker. While it is open it holds every beta; closed as completed it
# still holds the betas published before it closed. The issue's text is never read (its author could edit it).
# When the oldest waiting beta is 30 hours old (it missed a promotion it was due for), an issue "Promotion to stable
# has stalled" says so; it closes itself after the next promotion.
on:
schedule:
- cron: '7,37 20-21 * * *'
workflow_dispatch:
inputs:
tag:
description: The beta to promote now (empty = the newest beta that is due)
type: string
default: ''
force:
description: Promote that tag even if a release-blocker covers it or its smoke test didn't run
type: boolean
default: false
dry_run:
description: Say what would be promoted, change nothing
type: boolean
default: false
permissions:
contents: write
issues: write
concurrency:
group: promote
cancel-in-progress: false
jobs:
promote:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
# The notes of a promoted release cover every pull request since the last stable one.
fetch-depth: 0
# gh uses GH_TOKEN below; nothing needs git credentials on disk.
persist-credentials: false
- name: Promote
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag }}
FORCE: ${{ inputs.force && 'yes' || '' }}
DRY_RUN: ${{ (inputs.dry_run || github.ref != 'refs/heads/main') && 'yes' || '' }}
run: python3 tools/release_channels.py promote ${TAG:+--tag "$TAG"} ${FORCE:+--force} ${DRY_RUN:+--dry-run}
+16 -158
View File
@@ -1,16 +1,12 @@
name: release
# Automatic game releases (docs/release-flow.md). After every green `checks` run on main, every 10 minutes (so a
# release waiting for merges to settle goes out without another merge) and by hand, tools/release_train.py decides:
# a release when pull requests merged since the last one changed what players run and the newest merged 5 minutes
# ago (the oldest 20 minutes ago, if merges never pause), or at once for a pull request labelled release-now.
# a release when pull requests merged since the last one changed what players run and the newest merged 15 minutes
# ago (the oldest an hour ago, if merges never pause), or at once for a pull request labelled release-now.
# RELEASES_PAUSED=1 (repository variable) stops them.
# Builds Linux (an older Ubuntu, so the binary runs on any recent distribution and on NixOS through nix-ld) and
# Windows on GitHub, macOS on the Mac mini's runner (MAC_RUNNER=macos-latest sends it to GitHub), then
# publishes vX.Y.Z on the release line in .github/release-line.txt (docs/versioning.md; without it, the dated
# vYYYY.MM.DD[.N] from before 0.2) with notes from the merged pull requests, as a beta (a pre-release) that promote.yml makes
# stable in its daily promotion. macOS: with the repository variable MACOS_SIGN_IDENTITY set, the programs are signed with
# Developer ID and the hardened runtime and the zip is notarized, from the runner's signing keychain
# (packaging/macos/README.md); unset (forks), ad hoc.
# publishes vYYYY.MM.DD[.N] with notes from the merged pull requests.
# gh workflow run release --ref <branch> # dry run: builds and notes, publishes nothing
# gh workflow run release -f dry_run=false -f force=true # on main: release now, whatever the rules say
on:
@@ -45,7 +41,6 @@ jobs:
outputs:
release: ${{ steps.decide.outputs.release }}
tag: ${{ steps.decide.outputs.tag }}
version: ${{ steps.decide.outputs.version }}
head: ${{ steps.decide.outputs.head }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
@@ -66,9 +61,6 @@ jobs:
run: |
python3 tools/release_train.py decide --kind game ${HEAD_SHA:+--head "$HEAD_SHA"} ${FORCE:+--force} | tee decision.json
echo "### $(jq -r '.reason' decision.json)" >> "$GITHUB_STEP_SUMMARY"
# The version the programs are built with (docs/versioning.md): the tag without its v.
tag=$(jq -r '.tag' decision.json)
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
# A dry run builds whatever the decision, to test the builds.
if [[ "${{ steps.mode.outputs.dry_run }}" == true ]]; then echo "release=true" >> "$GITHUB_OUTPUT"; fi
@@ -84,9 +76,6 @@ jobs:
os: ubuntu-22.04
dir: horizon-linux-x86_64
asset: horizon-linux-x86_64.tar.gz
# The server build too: packaging/server/install.sh installs it on Linux.
server_dir: horizon-server-linux-x86_64
server_asset: horizon-server-linux-x86_64.tar.gz
- name: windows
os: windows-latest
dir: horizon-windows-x64
@@ -99,13 +88,9 @@ jobs:
dir: horizon-macos-arm64
asset: horizon-macos-arm64.zip
runs-on: ${{ matrix.mac && (vars.MAC_RUNNER || 'ludovics-mac-mini') || matrix.os }}
# Notarizing the zip can take a few minutes.
timeout-minutes: ${{ matrix.mac && 60 || 40 }}
timeout-minutes: 40
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# The release's version, built in (horizon_core::release): --version, user agents, crash reports and
# telemetry say it. No version-bump commits.
HORIZON_VERSION: ${{ needs.decide.outputs.version }}
defaults:
run:
shell: bash
@@ -114,11 +99,8 @@ jobs:
with:
ref: ${{ needs.decide.outputs.head }}
persist-credentials: false
# Always from a clean checkout, the Mac mini's runner too: its work folder is shared with pull requests'
# parity jobs, and nothing they leave (target/, an untracked .cargo/config.toml, a build script's output)
# may end up in a release. target/ is rebuilt from scratch; .local/ is relinked by vanilla-data.sh, which
# keeps its preparations outside the checkout.
clean: true
# On the Mac mini's runner, keep target/ (incremental) and .local/ (the prepared resources).
clean: ${{ runner.environment == 'github-hosted' }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
if: runner.environment == 'github-hosted'
@@ -127,34 +109,18 @@ jobs:
- name: ALSA and udev headers
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libudev-dev
- name: Build the client, horizon-ctl, horizon-prepare and horizon-server
run: cargo build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare -p horizon-server --bin horizon-server
- name: Build the client, horizon-ctl and horizon-prepare
run: cargo build --release --locked -p horizon-client --bin horizon-client --bin horizon-ctl -p horizon-prepare --bin horizon-prepare
- name: Every program the release folder takes was built
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
for binary in horizon-client horizon-ctl horizon-prepare; do
test -f "target/release/$binary${{ matrix.exe }}" || { echo "::error::target/release/$binary${{ matrix.exe }} is missing"; exit 1; }
done
- name: The signing keychain, unlocked
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: packaging/macos/keychain.sh unlock
- name: Signed with Developer ID and the hardened runtime
# Before the checks below, so they run the programs as players will.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
env:
MACOS_SIGN_IDENTITY: ${{ vars.MACOS_SIGN_IDENTITY }}
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --force --options runtime --timestamp --keychain "$MACOS_SIGN_KEYCHAIN" \
--identifier "dev.horizon.${binary#horizon-}" --sign "$MACOS_SIGN_IDENTITY" "target/release/$binary"
codesign --verify --strict --verbose=2 "target/release/$binary"
done
- name: Only system libraries
# Windows: no non-system DLL (a "DLL not found" dialog on some players' machines). Linux: the standard
# loader and libraries every desktop distribution has. macOS: only the system's own libraries.
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
for binary in horizon-client horizon-ctl horizon-prepare; do
file="target/release/$binary${{ matrix.exe }}"
if [[ "$RUNNER_OS" == macOS ]]; then
others=$(otool -L "$file" | tail -n +2 | awk '{print $1}' | grep -v -E '^(/usr/lib/|/System/Library/)' || true)
@@ -169,13 +135,8 @@ jobs:
bin=target/release
"$bin/horizon-client${{ matrix.exe }}" --help | grep -q -- '--resources'
"$bin/horizon-ctl${{ matrix.exe }}" --help > /dev/null
"$bin/horizon-prepare${{ matrix.exe }}" --version
"$bin/horizon-prepare${{ matrix.exe }}" --help | grep -q -- '--minecraft-dir'
# The version is the release's.
for binary in horizon-prepare horizon-server; do
out=$("$bin/$binary${{ matrix.exe }}" --version | tr -d '\r')
echo "$out"
[[ "$out" == "$binary $HORIZON_VERSION" ]] || { echo "::error::expected $binary $HORIZON_VERSION"; exit 1; }
done
- name: Tools from the runner
if: runner.environment == 'self-hosted'
run: bash tools/ci/runner-env.sh
@@ -193,7 +154,7 @@ jobs:
client="$PWD/target/release/horizon-client"
resources="$PWD/.local/horizon-client/$version"
(cd "$home" && HORIZON_HOME="$home" HORIZON_NO_AUDIO=1 "$client" --resources "$resources" --offscreen \
--world smoke --seed 1 --render-distance 4 --world-frames 1500 --timeout-seconds 180 \
--world smoke --seed 1 --render-distance 4 --frames 1500 --timeout-seconds 180 \
--smoke-report "$home/smoke.json" > "$home/client.log" 2>&1) || { tail -40 "$home/client.log"; exit 1; }
python3 - "$home/smoke.json" <<'EOF'
import json, sys
@@ -203,15 +164,6 @@ jobs:
or not report.get("meshed_sections") or not report.get("player"):
sys.exit("the smoke run did not draw a world")
EOF
# Published with the release: a beta becomes stable only with it (tools/release_channels.py).
cp "$home/smoke.json" "$RUNNER_TEMP/smoke-test.json"
- uses: actions/upload-artifact@v4
if: runner.environment == 'self-hosted'
with:
name: smoke-test.json
path: ${{ runner.temp }}/smoke-test.json
if-no-files-found: error
retention-days: 3
- name: Package
run: |
set -euo pipefail
@@ -221,102 +173,20 @@ jobs:
case "${{ matrix.asset }}" in
*.tar.gz) tar czf "${{ matrix.asset }}" "${{ matrix.dir }}" ;;
*) if [[ "$RUNNER_OS" == Windows ]]; then 7z a -tzip "${{ matrix.asset }}" "${{ matrix.dir }}" > /dev/null
else
# Without resource forks and extended attributes: plain ditto adds an
# AppleDouble `._` file beside every file that has any.
ditto -c -k --norsrc --noextattr --noqtn --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"
if unzip -Z1 "${{ matrix.asset }}" | grep -E '(^|/)(\._|__MACOSX/)'; then
echo "::error::macOS metadata files in ${{ matrix.asset }}"; exit 1
fi
fi ;;
else ditto -c -k --keepParent "${{ matrix.dir }}" "${{ matrix.asset }}"; fi ;;
esac
ls -l "${{ matrix.asset }}"
- name: Notarized
# A zip of programs can't be stapled: Gatekeeper asks Apple the first time each one runs.
if: runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: |
for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do
codesign --verify --strict "dist/${{ matrix.dir }}/$binary"
done
packaging/macos/notarize.sh "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: dist/${{ matrix.asset }}
if-no-files-found: error
retention-days: 3
- name: The server build (the game without sound or controllers, as horizon-engine)
if: matrix.server_asset
run: |
set -euo pipefail
# After the client is packaged: target/release/horizon-client is rebuilt without the audio and
# controllers features, so it needs neither libasound nor libudev (minimal server images lack them).
cargo build --release --locked -p horizon-client --no-default-features --bin horizon-client
cargo run --locked -p xtask -- package-server target/release/horizon-client "dist/${{ matrix.server_dir }}"
folder="dist/${{ matrix.server_dir }}"
python3 tools/check_imports.py --server "$folder/horizon-engine" "$folder/horizon-server" "$folder/horizon-prepare"
"$folder/horizon-engine" --help | grep -q -- '--resources'
[[ "$("$folder/horizon-server" --version)" == "horizon-server $HORIZON_VERSION" ]]
cd dist
rm -f "${{ matrix.server_asset }}"
tar czf "${{ matrix.server_asset }}" "${{ matrix.server_dir }}"
ls -l "${{ matrix.server_asset }}"
- uses: actions/upload-artifact@v4
if: matrix.server_asset
with:
name: ${{ matrix.server_asset }}
path: dist/${{ matrix.server_asset }}
if-no-files-found: error
retention-days: 3
- name: The signing keychain, locked again
if: always() && runner.os == 'macOS' && vars.MACOS_SIGN_IDENTITY != ''
run: packaging/macos/keychain.sh lock
sign:
publish:
needs: [decide, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job that holds the release signing key, a secret of the `release` environment, which only main's real
# releases enter (dry runs sign with a throwaway key). It runs no code from the repository but
# tools/ci/sign_release.sh, which calls only the runner's sha256sum and OpenSSL (docs/release-flow.md).
environment: ${{ needs.decide.outputs.dry_run != 'true' && 'release' || '' }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: tools/ci/sign_release.sh
sparse-checkout-cone-mode: false
persist-credentials: false
- uses: actions/download-artifact@v4
with:
path: dist
pattern: horizon-*
merge-multiple: true
- name: Sign the checksums (SHA256SUMS, SHA256SUMS.sig)
env:
RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
RELEASE_SIGNING_KEY_NEXT: ${{ secrets.RELEASE_SIGNING_KEY_NEXT }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
bash tools/ci/sign_release.sh "$TAG" dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz
- uses: actions/upload-artifact@v4
with:
name: signature
path: dist/SHA256SUMS*
if-no-files-found: error
retention-days: 3
- uses: actions/upload-artifact@v4
if: needs.decide.outputs.dry_run == 'true'
with:
name: ephemeral.pub
path: dist/ephemeral.pub
retention-days: 3
publish:
needs: [decide, build, sign]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
@@ -334,27 +204,15 @@ jobs:
with:
path: dist
merge-multiple: true
- name: The signature, checked as launchers check it
env:
TAG: ${{ needs.decide.outputs.tag }}
EPHEMERAL: ${{ needs.decide.outputs.dry_run == 'true' && 'yes' || '' }}
run: |
key=(); [[ -n "$EPHEMERAL" ]] && key=(--key "$(cat dist/ephemeral.pub)")
cargo run --locked --quiet -p horizon-release-sign -- verify --tag "$TAG" --publishing "${key[@]}" dist
- name: Publish ${{ needs.decide.outputs.tag }}
if: needs.decide.outputs.dry_run != 'true'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.decide.outputs.tag }}
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
smoke=(); [[ -f dist/smoke-test.json ]] && smoke=(dist/smoke-test.json)
# A beta (pre-release), the Alpha channel's: promote.yml makes it stable and latest in its daily promotion
# (docs/release-flow.md).
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --prerelease --latest=false dist/horizon-linux-x86_64.tar.gz \
dist/horizon-windows-x64.zip dist/horizon-macos-arm64.zip dist/horizon-server-linux-x86_64.tar.gz \
dist/SHA256SUMS "${signature[@]}" "${smoke[@]}"
--notes-file notes/body.md --latest dist/horizon-linux-x86_64.tar.gz dist/horizon-windows-x64.zip \
dist/horizon-macos-arm64.zip
- name: Dry run, the release that would be published
if: needs.decide.outputs.dry_run == 'true'
run: |
-24
View File
@@ -1,24 +0,0 @@
name: run-parity
# A fork's pull request runs the parity job (parity.yml) on the Mac mini only when a maintainer who has read the
# change adds the run-parity label: once, on the commit the pull request has then. Remove the label and add it
# again after new pushes. A separate workflow, so that labels never start `checks`: there a label's run reported a
# skipped `verify`, and GitHub counts a skipped required check as passed.
on:
pull_request:
types: [labeled]
permissions:
contents: read
concurrency:
group: run-parity-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
# Test runs never reach the telemetry server: no live-count ping, no uploads (docs/telemetry.md).
HORIZON_TELEMETRY_URL: "off"
jobs:
parity:
if: github.event.label.name == 'run-parity'
uses: ./.github/workflows/parity.yml
with:
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
-3
View File
@@ -24,10 +24,7 @@
/horizon.txt
/horizon-graphics.txt
/horizon-controller.txt
/horizon-extras.txt
/horizon-player.txt
/servers.dat
/screenshots/
/.horizon-cache/
/crash-reports/
/debug/
-9
View File
@@ -4,9 +4,6 @@ Thanks for helping. Horizon is Minecraft: Java Edition 26.3 rewritten in Rust, a
everything is that **it plays exactly like vanilla**. This page is the short version of how we work; questions are
welcome on [Discord](https://discord.gg/HcTeMZxe5E).
This repository holds the game, the launcher and the website, plus the tools, tests and docs that build, check and
release them. Anything else (videos, Discord and other community material) lives elsewhere.
## Build it
You need Rust (stable), Python 3.10+, JDK 25, and on Linux the ALSA and udev headers (`libasound2-dev libudev-dev`).
@@ -53,11 +50,6 @@ crates you touched (and the launcher's), generated files, `Cargo.lock`, the Pyth
your change can affect. Without the vanilla data on your machine it lists those harnesses instead; CI runs them.
`--quick` skips the harnesses. It exits non-zero when something fails.
For the game workspace, Clippy checks all targets of the touched crates and their transitive workspace dependents,
including examples. The dependency scope includes optional, platform, build and development dependencies.
Tests run library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
## Pull requests
- **Small.** One change per pull request, under about 500 lines of code where you can; split bigger work into
@@ -71,7 +63,6 @@ configuration and toolchain changes select the whole workspace; CI also keeps it
`release-now` is for crash fixes that shouldn't wait. pr_ready suggests one.
Every merge to main is released automatically ([release flow](docs/release-flow.md)), so main always stays playable.
How agents, checks, releases and signed updates keep this safe: [security](docs/security.md).
## Parity checks on forks
Generated
+12 -1470
View File
File diff suppressed because it is too large. Load diff
+2 -10
View File
@@ -1,6 +1,6 @@
[workspace]
exclude = ["launcher", "crates/horizon-telemetry-server", "third_party/wgpu-hal"]
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
exclude = ["launcher", "crates/horizon-telemetry-server"]
members = ["crates/horizon-core", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "xtask"]
resolver = "2"
[workspace.package]
@@ -16,14 +16,6 @@ type_complexity = "allow"
needless_range_loop = "allow"
large_enum_variant = "allow"
# wgpu-hal 30.0.1 with gfx-rs/wgpu#10124: wgpu 30 creates a Vulkan descriptor
# pool with no pool sizes while it creates the device, which Vulkan allows only
# since 1.3.215, and Intel's Windows driver for 6th-10th gen iGPUs (HD 520/530,
# UHD 620/630) crashes on it with an access violation (#746). Drop this and the
# folder once a wgpu release has the fix.
[patch.crates-io]
wgpu-hal = { path = "third_party/wgpu-hal" }
# Line tables keep file:line in backtraces; full debug info made every
# example and test binary hundreds of megabytes and slow to link.
[profile.dev]
+77 -39
View File
@@ -14,64 +14,104 @@
<p align="center">
<a href="https://playhorizon.dev"><b>Website</b></a> ·
<a href="#download"><b>Download</b></a> ·
<a href="https://playhorizon.dev/docs/"><b>Documentation</b></a> ·
<a href="https://github.com/ludovic111/horizon/releases"><b>Release notes</b></a> ·
<a href="#project-status"><b>Project status</b></a> ·
<a href="#contributing"><b>Contributing</b></a>
<a href="#what-changed-in-v202610131"><b>What changed</b></a> ·
<a href="#screenshots"><b>Screenshots</b></a> ·
<a href="#project-status"><b>Parity status</b></a> ·
<a href="docs/README.md"><b>Documentation</b></a>
</p>
# Horizon
Horizon is Minecraft: Java Edition 26.3 rewritten in Rust: a native game that reads the same worlds, follows the same rules and generates the same terrain as the Java game, and is checked against it by 281 differential harnesses. You play it through a small launcher, on the files from your own copy of Minecraft. It is free, open source and unofficial.
A Rust rewrite of Minecraft: Java Edition 26.3 built for one-to-one vanilla parity, checked against the game itself with differential tests.
Every subsystem matches vanilla in all 281 parity harnesses, on Linux and macOS: 100% of what the harnesses compare (see [Project status](#project-status)). The native client is playable on Linux, macOS (Apple Silicon) and Windows.
> Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own legally obtained copy of Minecraft: Java Edition 26.3.
## What changed in v2026.10.13.1
The latest release ([notes](https://github.com/ludovic111/horizon/releases/tag/v2026.10.13.1)):
- **Horizon's own menus:** a new title screen, world list, options and pause menu in the launcher's style, over a live frozen landscape. The vanilla look is still available: Options → Interface → Menu style → Classic.
- **Graphics settings, Simple and Advanced:** presets (Performance, Balanced, Quality, Ultra), or every rendering, quality, display and performance setting, each with a short description and its cost, plus a performance overlay.
- **Much faster rendering:** for example, at render distance 8 over a village, 363 → 2,409 FPS on an RTX 4080. Render distance 32 still stutters in places.
- **Fixed:** no sound on Linux; Linux builds that only ran on the machine they were built on; a missing DLL on Windows (the launcher no longer needs the Visual C++ runtime); memory growing and the frame rate dropping near villages (mobs were duplicated when chunks reloaded); resource preparation failing on Linux with "Missing … java-objc-bridge" ([#41](https://github.com/ludovic111/horizon/issues/41)); the hand and held item at high FOV; missing swing animations.
## Download
Horizon is installed through **the Horizon launcher**. Get it from [playhorizon.dev](https://playhorizon.dev) (the Download button picks your system) or from [the launcher releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true), for Linux x86-64, macOS on Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its files from your own copy of Minecraft, and updates itself.
Horizon is downloaded only through **the Horizon launcher**: get it from [playhorizon.dev](https://playhorizon.dev) or from [the launcher's releases](https://github.com/ludovic111/horizon/releases?q=launcher-v&expanded=true) (the newest `launcher-v*` release, currently [launcher-v0.1.3](https://github.com/ludovic111/horizon/releases/tag/launcher-v0.1.3)), for Linux x86-64, macOS Apple Silicon or Windows x64. The launcher installs and updates the game, prepares its resources from your own copy of Minecraft, starts the client or a dedicated server, and updates itself.
**You need** Minecraft: Java Edition 26.3, played once (the official launcher, Prism, MultiMC and other launchers all work); a Java 25 JDK, for the one-time preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac. Step by step, with macOS's first-open prompt and the first launch: [Install and first launch](https://playhorizon.dev/docs/install).
**You need** Minecraft: Java Edition installed with the official launcher, with 26.3 played once; Python 3.10 or newer and Java 25 (a JDK, e.g. Eclipse Temurin 25) for the one-time resource preparation; and a GPU with Vulkan on Linux and Windows, or any Apple Silicon Mac.
### The launcher
<table>
<tr>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: a version ready to play, its summary, Client and Server, the version picker and Play"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page"></td>
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Play page: v2026.10.14.1 ready to play, its summary, Client and Server, the version picker and Play, the resources and the Minecraft folder as a numbered list"></td>
<td width="50%"><img src="docs/media/launcher-patch-notes.webp" alt="The launcher's Patch notes page, showing the notes of Horizon 2026.10.14.1"></td>
</tr>
</table>
## What works today
It lists Horizon's releases and installs the build for your platform, shows each release's patch notes, updates Horizon and itself, and doubles as a command-line tool and an MCP server. See [the launcher's README](launcher/README.md).
The [documentation](https://playhorizon.dev/docs/) covers each of these.
## Screenshots
- **The game:** singleplayer with vanilla's world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on saves in vanilla's format. Worlds from 1.8 to 26.2 are upgraded as vanilla does. [Status table](#project-status).
- **Horizon's own extras:** new menus (vanilla's are one setting away), graphics presets, distant terrain up to 1024 chunks, controller support, optional zoom, minimap and more, all off by default where they would change the vanilla look. See [Settings](https://playhorizon.dev/docs/settings).
- **Resource packs, shader packs and data packs:** resource packs work, Java shader packs run their full-screen effects (experimental), and data packs load functions, predicates and item modifiers.
- **Playing with others:** invite friends from the pause menu's Friends button with a code, Open to LAN, and joining offline-mode servers. Horizon cannot sign in to a Microsoft account yet, so it cannot join servers that check accounts. See [Multiplayer](https://playhorizon.dev/docs/multiplayer).
- **Mods:** Horizon has its own mod system (Rust compiled to WebAssembly). Java mod jars do not run. See [Mods and data packs](https://playhorizon.dev/docs/mods).
Captured in the Rust client itself, with vanilla assets and no mods or shaders.
Screenshots, captured in the Rust client itself with vanilla assets and no mods or shaders:
### Menus and settings
<table>
<tr>
<td colspan="2"><img src="docs/media/menu-title.webp" alt="Horizon's title screen: the mark and the pages on the left, the Horizon title, Singleplayer and Multiplayer, and the last world played and the faint wordmark, over a veiled snowy landscape"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/menu-worlds.webp" alt="The Singleplayer page: three worlds with their dates and game modes, and Create world and Play buttons"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="The Video settings in their Simple view: a preset, render distance, field of view, framerate, VSync and GUI scale, each with what it costs"></td>
</tr>
</table>
The vanilla menus are one setting away: Options → Interface → Menu style → Classic. See [Graphics settings](docs/graphics.md).
### In game
<table>
<tr>
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics, Advanced Graphics and Shader Packs"></td>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village with farms, an iron golem and animals"></td>
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie attacks near the village"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
<td width="50%"><img src="docs/media/night.webp" alt="Moonrise over a torch-lit village and river"></td>
</tr>
<tr>
<td width="50%"><img src="docs/media/underwater.webp" alt="Salmon swimming in a river"></td>
<td width="50%" valign="middle">
Vanilla world generation ported to Rust (terrain, aquifers, carvers, features, villages and the Nether), every block model with smooth lighting and biome tints, the vanilla sky and fog, mobs, the HUD, chat and advancements: everything in these frames is drawn by Horizon.
</td>
</tr>
</table>
## Host a server
## What it does
The launcher's **Servers** page creates, starts, backs up and updates a Horizon server with buttons, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and runs without Java. Guide: [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
Horizon reimplements the Minecraft Java client and its game logic in Rust, one subsystem at a time, and compares each piece with the vanilla game. Each `tools/test_*_parity.py` harness drives the same inputs through Horizon and through the vanilla client and server bytecode from your own copy of the game, then checks that the outputs are identical. It is for players and developers interested in game reimplementation, and for anyone who wants to see how far a behaviour-exact native rewrite can go.
## Modpacks
The [native client](docs/native-client.md) opens on a vanilla-style title screen with world creation and selection, and generates worlds with the Rust port of the vanilla generators: noise terrain, aquifers, surface rules, carvers and placed features, all checked against vanilla. It streams and lights chunks on worker threads and renders every vanilla block model with smooth lighting, biome tints, the vanilla sky, sun, moon, stars and clouds. Gameplay includes:
The launcher's **Modpacks** page imports a Modrinth or CurseForge pack, shows which of its mods Horizon can carry over, and plays the ones it can as their own profile. Today that means content mods made of data and assets; mods with Java code are not ported yet. See [Modpacks](https://playhorizon.dev/docs/modpacks).
- validated player physics, health and hunger;
- mining with vanilla tool rules and loot tables;
- placing, crafting, chests and furnaces;
- dropped items, particles and sounds;
- passive and hostile mobs with combat and vanilla AI, including guardians, shulkers, illagers, ravagers, the wither, the ender dragon fight, the breeze, the creaking, the sniffer, the happy ghast, llamas and the copper golem;
- villagers that take jobs, trade, restock, meet at the bell and sleep in beds, on a port of vanilla's brain;
- raids and pillager patrols;
- dispensers, crafters, the loom and the recipe book;
- every command, scoreboards and teams, the player list, subtitles and the HUD overlays;
- boats, minecarts, item frames, paintings and armor stands;
- shields, maps, books, fireworks, beacons, advancements and statistics;
- Anvil saves, joining vanilla servers, and opening a world to LAN.
## Agent control
@@ -107,18 +147,7 @@ cargo run --release -p horizon-client
Everything prepared lands in the gitignored `.local/`. It is derived from the game: never commit it or share it.
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, scripting through `horizon-ctl` and the architecture are described in [the native client guide](docs/native-client.md). The rest of the developer documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels), or [the contributing guide](https://playhorizon.dev/docs/contributing) on the site. Questions and ideas: [Discord](https://discord.gg/HcTeMZxe5E). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a maintainer's computer holding their own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
The client starts on the title screen. Command-line options (`--world`, `--seed`, `--creative`, `--render-distance`, `--dedicated`), controls, playing and scripting through `horizon-ctl` and its MCP server, and the architecture are described in [the native client guide](docs/native-client.md). The rest of the documentation is indexed in [docs/README.md](docs/README.md), including [the parity workflow](docs/parity-workflow.md).
## Project status
@@ -153,14 +182,23 @@ them too (Rust uses Java's own float maths on each platform).
### Known issues
Horizon can't sign in to a Microsoft account yet, so it can't join online-mode servers (it joins offline-mode ones); skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
Online-mode servers (Mojang's session servers) are not supported; skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; the creative hotbar keys (C/X) aren't wired to the hotbar loader; item tooltips lack attribute lines.
## Contributing
Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) for how we work (vanilla first, small pull requests, labels). Before opening a pull request, run the self-check and paste the block it prints:
```sh
git config core.hooksPath .githooks
python3 tools/pr_ready.py
```
It runs rustfmt, clippy with warnings denied, the tests of the crates you touched, the Python tests, the public-tree check, and the vanilla harnesses your change can affect. CI runs the same checks on GitHub's Linux runners; the vanilla harnesses need the game, which Horizon never downloads, so they run on a self-hosted runner holding the maintainer's own copy. Never commit game files, decompiled source or prepared resources: `tools/check_public_tree.py` rejects them, and tracked Java is allowed only under the project's own `dev/horizonlab` namespace.
## Licence
[MIT](LICENSE), for the project's original work only. Dependencies keep their own licences. Minecraft is a trademark of Mojang Synergies AB; Minecraft code and assets belong to Mojang and Microsoft and are covered by the [Minecraft EULA](https://www.minecraft.net/en-us/eula). Decompiled game source and prepared resources stay on your machine and are never published.
Third-party credits and licenses: [Third-party notices](THIRD_PARTY_NOTICES.md).
## Author
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
-50
View File
@@ -1,50 +0,0 @@
# Third-party notices
## CullLeaves
The optional leaf and mangrove-root face-culling behavior is adapted from
[CullLeaves](https://github.com/TeamMidnightDust/CullLeaves) by MidnightDust.
Its MIT license is reproduced below.
```text
MIT License
Copyright (c) 2020 MidnightDust
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## wgpu-hal
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
developers, with one upstream fix applied
([gfx-rs/wgpu#10124](https://github.com/gfx-rs/wgpu/pull/10124)). It is licensed under MIT or
Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
## Pixelify Sans
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
by The Pixelify Sans Project Authors, licensed under the SIL Open Font License 1.1
(`crates/horizon-theme/fonts/OFL-PixelifySans.txt`).
## Pixelarticons
`crates/horizon-theme/icons/pixel/*.svg` are icons from
[Pixelarticons](https://github.com/halfmage/pixelarticons) by Gerrit Halfmann, under the MIT
license (`crates/horizon-theme/icons/pixel/LICENSE-pixelarticons.txt`).
-174
View File
@@ -1,174 +0,0 @@
# Dedicated server against vanilla 26.3, with bots (2026-10-17)
`horizon-client --dedicated` (run by `horizon-server`) against the pinned vanilla 26.3 `server.jar`, on `main`
at `7bbbf6e9` (in release v2026.10.17.17). That build has the movement checks and chunk pacing of #586, the
equal-work method of #670, and the server performance PRs #701–#707 and #713.
This replaces the data first published here, which came from a Mac mini overloaded by other work (load 55–141 on
14 threads). An earlier run of this method, on `main` at `002f14ce`, is kept in `earlier-002f14ce/` and compared
at the end.
## Setup
- **Server machine:** ThinkPad, Intel i7-7500U (2 cores, 4 threads), 32 GB, NixOS. Nothing else heavy ran on it:
its load average was 0.4 when idle. Both servers ran under `nice -n 19`.
- **Bots:** `horizon-bots` on another machine (Mac mini M4 Pro), over the tailnet. That machine was shared with
other work (load 36–170 on 14 threads, mean 78). The bots need little CPU, but a busy bot host can answer later.
Both servers saw the same conditions, since they took turns at each bot count.
- **Horizon:** release build of `7bbbf6e9` (`horizon-server`, `horizon-client`).
- **Vanilla:** the pinned 26.3 `server.jar` on OpenJDK 25.0.4.1, with `-Xms4g -Xmx4g`.
- **Properties:**
- `level-seed=12345`, view and simulation distance 10, creative (forced);
- `online-mode=false`, `difficulty=easy`, `max-players=200`;
- default game rules on both (the locator bar is on).
- **Each run:** a fresh server, bots joining one per second, a 60 s warm-up, then 180 s measured.
- **Commands** (on the server machine):
```
python3 tools/server_bench.py --servers horizon,vanilla --nice 19 --warmup 60 --seconds 180 \
--bots-via "ssh <bot host>" --bots-bin <horizon-bots> --connect <server address> \
--bots 1,10,25,50,100 --behaviour walk,explore
python3 tools/server_bench.py ...same... --bots 1,10,25,50 --behaviour explore --explore-speed 0.8
```
- **Columns:**
- **MSPT** (mean) and **P95** come from `/tick query`, in ms. **TPS** is the tick rate.
- **RSS** is the mean over the run. **CPU** is the whole process tree, as a percentage of one core.
- **Chunks/s** is what the bots received. **New chunks/s** counts each chunk once per bot.
- **Pulled back** counts the times the server moved a bot back (vanilla's "moved too quickly" and similar).
- **Load** is the mean load average during that run: the server machine (which includes the server's own
threads) and the bot host.
## Results
### Equal work: bots explore at the same pace (`--explore-speed 0.8`)
Each bot flies outwards at 0.8 blocks a second, which both servers keep up with. They send the same chunks, so
the tick times compare the same work.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 3.16 | 5.78 | 20.0 | 0.76 GB | 22 % | 2 | 1.27 | 0 | 2.8 / 63 |
| 1 | Vanilla | 16.82 | 27.3 | 20.0 | 3.37 GB | 69 % | 2 | 1.35 | 0 | 1.8 / 94 |
| 10 | Horizon | 9.83 | 19.01 | 20.0 | 0.86 GB | 77 % | 14 | 12.31 | 1051 | 2.2 / 74 |
| 10 | Vanilla | 24.38 | 39.58 | 20.0 | 3.57 GB | 132 % | 14 | 12.32 | 1027 | 2.5 / 64 |
| 25 | Horizon | 16.87 | 30.88 | 20.0 | 0.89 GB | 111 % | 34 | 29.92 | 3770 | 5.1 / 51 |
| 25 | Vanilla | 30.29 | 43.47 | 20.0 | 3.61 GB | 176 % | 33 | 29.29 | 5139 | 5.2 / 55 |
| 50 | Horizon | 23.57 | 47.53 | 20.0 | 0.92 GB | 127 % | 67 | 59.42 | 7434 | 8.5 / 64 |
| 50 | Vanilla | 42.44 | 54.84 | 20.0 | 3.67 GB | 201 % | 65 | 58.11 | 10604 | 5.1 / 59 |
### `walk`: bots walk around the spawn
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 4.01 | 8.19 | 20.0 | 0.78 GB | 59 % | 10 | 0.43 | 0 | 1.4 / 70 |
| 1 | Vanilla | 18.22 | 29.34 | 20.0 | 3.43 GB | 69 % | 10 | 0.47 | 0 | 1.9 / 63 |
| 10 | Horizon | 8.79 | 17.35 | 20.0 | 0.78 GB | 119 % | 95 | 3.58 | 0 | 2.9 / 69 |
| 10 | Vanilla | 18.67 | 38.26 | 20.0 | 3.50 GB | 101 % | 101 | 4.38 | 5 | 3.2 / 59 |
| 25 | Horizon | 15.01 | 31.12 | 20.0 | 0.80 GB | 161 % | 234 | 8.49 | 0 | 6.3 / 43 |
| 25 | Vanilla | 18.78 | 28.16 | 20.0 | 3.52 GB | 155 % | 252 | 11.89 | 10 | 6.7 / 57 |
| 50 | Horizon | 22.72 | 60.19 | 20.0 | 0.81 GB | 180 % | 465 | 16.94 | 0 | 14.7 / 50 |
| 50 | Vanilla | 34.3 | 55.55 | 20.0 | 3.58 GB | 267 % | 497 | 26.74 | 39 | 9.5 / 56 |
| 100 | Horizon | 63.33 | 125.04 | 15.79 | 0.87 GB | 246 % | 678 | 31.8 | 0 | 32.9 / 95 |
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 15.1 / 120 |
Both servers sent each walking bot the same number of distinct chunks over the whole run (680–690). Horizon's
"new chunks/s" inside the measured window is lower, so compare the totals, not that column.
### `explore` at full speed: unequal work
Bots fly outwards as fast as chunks arrive (`horizon-bots`' default, up to 22 blocks a second). Both servers now pull them back ("moved too quickly"), but Horizon sends
more new terrain, so this pass doesn't compare equal work. The server-machine load of Horizon's 1-bot run is
estimated from the time between the runs around it: its log was overwritten before it was read.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s | Pulled back | Load (server / bots) |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 9.73 | 20.99 | 20.0 | 0.84 GB | 330 % | 41 | 35.96 | 0 | 4.7 / 129 |
| 1 | Vanilla | 9.71 | 20.37 | 20.0 | 3.61 GB | 247 % | 35 | 33.61 | 0 | 4.3 / 156 |
| 10 | Horizon | 21.27 | 39.15 | 20.0 | 1.18 GB | 322 % | 46 | 46.11 | 9856 | 5.0 / 114 |
| 10 | Vanilla | 16.94 | 30.28 | 20.0 | 3.95 GB | 254 % | 25 | 25.01 | 11123 | 5.5 / 89 |
| 25 | Horizon | 25.76 | 45.01 | 20.0 | 1.17 GB | 314 % | 61 | 60.7 | 31264 | 6.6 / 82 |
| 25 | Vanilla | 24.93 | 37.09 | 20.0 | 4.04 GB | 305 % | 38 | 37.84 | 33467 | 6.1 / 91 |
| 50 | Horizon | 39.31 | 65.33 | 20.0 | 1.25 GB | 325 % | 115 | 112.25 | 64163 | 6.9 / 95 |
| 50 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 7.7 / 86 |
| 100 | Horizon | 52.7 | 85.73 | 18.97 | 1.36 GB | 328 % | 154 | 151.83 | 116161 | 13.4 / 79 |
| 100 | Vanilla | stopped by its watchdog (a 60 s tick) | | | | | | | | 10.0 / 72 |
Vanilla's watchdog stopped it after a 60-second tick in three runs (walk 100, explore 50 and 100). Each time its
server thread was in entity tracking (`ChunkMap$TrackedEntity.updatePlayer`).
## What it shows
- **Same work, 1 to 50 players:** Horizon's mean tick is 44–81 % shorter than vanilla's, its 95th percentile is
lower, and its process uses 32–63 % of vanilla's CPU.
- **Walking, 1 to 50 players:** Horizon's mean tick is shorter at every count (4.0 against 18.2 ms with 1 bot,
22.7 against 34.3 ms with 50).
- **Where vanilla wins:**
- With 25 and 50 walking bots, vanilla's 95th percentile is lower (28.2 against 31.1 ms, 55.6 against
60.2 ms). Horizon's slow ticks are spikier.
- In the full-speed explore pass, vanilla's mean tick is shorter with 10 and 25 bots (16.9 against 21.3 ms,
24.9 against 25.8 ms), while Horizon sends 1.6–1.8 times as many new chunks.
- With 50 walking bots, Horizon makes the server machine busier (load 14.7 against 9.5). Its connection
writer threads compress every packet (below).
- **100 players:** Horizon keeps 15.8 TPS walking and 19.0 TPS exploring at full speed. Vanilla's watchdog
stopped it in both.
- **Memory:** Horizon used 0.76–0.92 GB in every walk and equal-work run, and 1.36 GB at most when 100 bots
explored at full speed. Vanilla used 3.4–4.0 GB with its 4 GB heap.
- **Start:** Horizon's fresh world was ready in 9.2–9.9 s, vanilla's in 14.9–17.2 s.
## Where Horizon's time goes (50 walking bots)
`perf record -e cycles:u --call-graph lbr` on the engine for 30 s, during a separate 50-bot `walk` run on the same
build and machine (MSPT 21.7 ms, 178 % CPU). Shares of all the engine's CPU samples:
| Thread | Share | Mostly |
|---|---:|---|
| Connection writers (`horizon-lan-out`) | 55 % | 53 % in `miniz_oxide` deflate, from `horizon_protocol::frame::encode` |
| World generation workers | 33 % | noise, surface rules, aquifers |
| Server thread (the tick) | 12 % | see below |
Inside the tick:
- the locator bar (`Waypoints::tick`): a third of the tick;
- `Hub::tick`: a quarter;
- block entities, creatures, chunk encoding (`ChunkEncoder::encode_in`) and `Streaming::update`: 7–10 % each;
- SipHash of `u128` keys (the default `HashMap` hasher): about 10 % of the tick.
**The next biggest cost is compressing packets.** Every chunk packet is compressed again for each player who gets
it, with `flate2`'s default `miniz_oxide` backend at level 6. On this 4-thread CPU that competes with the tick
and with world generation. Two fixes, which can be combined:
- the `zlib-rs` backend for `flate2`: 2.2–10 times faster than `miniz_oxide` at level 6 in a quick test, with
output about 3 % larger;
- compressing a chunk packet once and sending the same bytes to every player who needs it.
## Before and after the performance PRs
The earlier run, on `main` at `002f14ce` with #670's bench changes (build `a9ee56a8`), before #586, #701–#707 and
#713. Same machines and settings. MSPT mean / P95, in ms:
Walk:
| Bots | Horizon before | Horizon now | Vanilla before | Vanilla now |
|---:|---|---|---|---|
| 1 | 5.77 / 11.04 | 4.01 / 8.19 | 18.84 / 29.55 | 18.22 / 29.34 |
| 10 | 16.23 / 29.32 | 8.79 / 17.35 | 18.02 / 37.62 | 18.67 / 38.26 |
| 25 | 23.98 / 49.18 | 15.01 / 31.12 | 19.19 / 33.84 | 18.78 / 28.16 |
| 50 | 35.07 / 73.24 | 22.72 / 60.19 | 35.23 / 60.87 | 34.3 / 55.55 |
| 100 | 104.8 / 158.8 | 63.33 / 125.04 | watchdog | watchdog |
Equal work:
| Bots | Horizon before | Horizon now | Vanilla before | Vanilla now |
|---:|---|---|---|---|
| 1 | 4.78 / 7.3 | 3.16 / 5.78 | 16.63 / 26.23 | 16.82 / 27.3 |
| 10 | 15.19 / 25.66 | 9.83 / 19.01 | 24.65 / 40.81 | 24.38 / 39.58 |
| 25 | 23.37 / 42.59 | 16.87 / 30.88 | 30.28 / 44.81 | 30.29 / 43.47 |
| 50 | 30.59 / 67.94 | 23.57 / 47.53 | 43.52 / 56.18 | 42.44 / 54.84 |
Vanilla's numbers repeat within a few percent, so the runs are comparable. In the earlier run, 25 walking bots
were faster on vanilla (19.2 against 24.0 ms). A profile then put a third of the tick in advancement updates,
which #701 fixed.
## Files
- `walk-explore.json`: the `walk` and full-speed `explore` passes. `explore-equal-rate.json`: the equal-work pass.
- `load.txt`: both machines' load averages every 30 s during the runs.
- `earlier-002f14ce/`: the same files for the earlier run.
- The server's tailnet address is replaced by `<server address>` in the JSON.
@@ -1,341 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T15:37:03+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "a9ee56a8b143999c9bc7056850ffcd622a9214aa",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/explore-equal-rate.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.21,
"mspt": {
"average": 4.78,
"p50": 4.59,
"p95": 7.3,
"p99": 13.53
},
"mspt_worst_p99": 36.5,
"tps": 20.0,
"rss_mb": {
"mean": 759.91,
"max": 771.3
},
"cpu_percent": 24.49,
"chunks_per_second": 1.64,
"unique_per_second": 1.27,
"forgotten_per_second": 1.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 841,
"chunks_per_bot": {
"explore": 841.0
},
"unique_chunks_per_bot": {
"explore": 781.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 14.35,
"mspt": {
"average": 16.63,
"p50": 16.73,
"p95": 26.23,
"p99": 35.39
},
"mspt_worst_p99": 68.5,
"tps": 20.0,
"rss_mb": {
"mean": 3431.12,
"max": 3459.2
},
"cpu_percent": 69.76,
"chunks_per_second": 1.66,
"unique_per_second": 1.37,
"forgotten_per_second": 1.67,
"waited_ticks_per_second": 0.0,
"chunks_total": 840,
"chunks_per_bot": {
"explore": 840.0
},
"unique_chunks_per_bot": {
"explore": 780.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 15.19,
"p50": 14.45,
"p95": 25.66,
"p99": 38.89
},
"mspt_worst_p99": 57.6,
"tps": 20.0,
"rss_mb": {
"mean": 859.86,
"max": 920.2
},
"cpu_percent": 84.95,
"chunks_per_second": 14.22,
"unique_per_second": 12.7,
"forgotten_per_second": 14.11,
"waited_ticks_per_second": 0.0,
"chunks_total": 7962,
"chunks_per_bot": {
"explore": 796.2
},
"unique_chunks_per_bot": {
"explore": 768.2
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 14.74,
"mspt": {
"average": 24.65,
"p50": 21.82,
"p95": 40.81,
"p99": 70.23
},
"mspt_worst_p99": 119.1,
"tps": 20.0,
"rss_mb": {
"mean": 3528.77,
"max": 3643.2
},
"cpu_percent": 133.1,
"chunks_per_second": 13.62,
"unique_per_second": 12.32,
"forgotten_per_second": 13.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 7746,
"chunks_per_bot": {
"explore": 774.6
},
"unique_chunks_per_bot": {
"explore": 748.7
},
"resent": 0,
"pulled_back": 1150,
"moved_too_quickly": 5,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.42,
"mspt": {
"average": 23.37,
"p50": 21.61,
"p95": 42.59,
"p99": 60.21
},
"mspt_worst_p99": 102.1,
"tps": 20.0,
"rss_mb": {
"mean": 889.21,
"max": 950.7
},
"cpu_percent": 117.59,
"chunks_per_second": 35.34,
"unique_per_second": 31.62,
"forgotten_per_second": 35.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 20137,
"chunks_per_bot": {
"explore": 805.48
},
"unique_chunks_per_bot": {
"explore": 774.52
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 15.14,
"mspt": {
"average": 30.28,
"p50": 28.17,
"p95": 44.81,
"p99": 71.77
},
"mspt_worst_p99": 158.1,
"tps": 20.0,
"rss_mb": {
"mean": 3574.32,
"max": 3692.7
},
"cpu_percent": 173.43,
"chunks_per_second": 32.76,
"unique_per_second": 29.38,
"forgotten_per_second": 33.02,
"waited_ticks_per_second": 0.0,
"chunks_total": 19417,
"chunks_per_bot": {
"explore": 776.68
},
"unique_chunks_per_bot": {
"explore": 747.36
},
"resent": 0,
"pulled_back": 4728,
"moved_too_quickly": 15,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.11,
"mspt": {
"average": 30.59,
"p50": 25.19,
"p95": 67.94,
"p99": 90.77
},
"mspt_worst_p99": 122.1,
"tps": 20.0,
"rss_mb": {
"mean": 915.93,
"max": 978.0
},
"cpu_percent": 132.13,
"chunks_per_second": 71.31,
"unique_per_second": 63.56,
"forgotten_per_second": 71.19,
"waited_ticks_per_second": 0.0,
"chunks_total": 41365,
"chunks_per_bot": {
"explore": 827.3
},
"unique_chunks_per_bot": {
"explore": 793.14
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 15.35,
"mspt": {
"average": 43.52,
"p50": 41.69,
"p95": 56.18,
"p99": 84.55
},
"mspt_worst_p99": 155.1,
"tps": 20.0,
"rss_mb": {
"mean": 3801.29,
"max": 3920.7
},
"cpu_percent": 198.97,
"chunks_per_second": 65.86,
"unique_per_second": 58.63,
"forgotten_per_second": 66.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 39790,
"chunks_per_bot": {
"explore": 795.8
},
"unique_chunks_per_bot": {
"explore": 764.48
},
"resent": 0,
"pulled_back": 8392,
"moved_too_quickly": 38,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
}
]
}
@@ -1,278 +0,0 @@
2026-10-17T13:17:13Z thinkpad: 2.76 2.26 2.04 | macmini (bots): { 73.08 70.73 75.98 }
2026-10-17T13:17:43Z thinkpad: 2.90 2.33 2.07 | macmini (bots): { 75.66 71.62 76.12 }
2026-10-17T13:18:13Z thinkpad: 3.02 2.43 2.11 | macmini (bots): { 65.77 69.61 75.24 }
2026-10-17T13:18:44Z thinkpad: 2.15 2.27 2.07 | macmini (bots): { 59.85 67.75 74.37 }
2026-10-17T13:19:14Z thinkpad: 1.45 2.09 2.02 | macmini (bots): { 63.86 67.87 74.17 }
2026-10-17T13:19:44Z thinkpad: 1.28 1.98 1.98 | macmini (bots): { 63.51 67.54 73.84 }
2026-10-17T13:20:14Z thinkpad: 1.14 1.89 1.95 | macmini (bots): { 65.82 67.85 73.71 }
2026-10-17T13:20:45Z thinkpad: 0.90 1.77 1.91 | macmini (bots): { 63.62 67.08 73.22 }
2026-10-17T13:21:15Z thinkpad: 0.55 1.60 1.85 | macmini (bots): { 62.45 66.37 72.75 }
2026-10-17T13:21:45Z thinkpad: 0.79 1.54 1.82 | macmini (bots): { 67.04 67.04 72.76 }
2026-10-17T13:22:15Z thinkpad: 1.96 1.75 1.88 | macmini (bots): { 65.53 66.60 72.40 }
2026-10-17T13:22:46Z thinkpad: 1.82 1.75 1.88 | macmini (bots): { 61.52 65.55 71.82 }
2026-10-17T13:23:16Z thinkpad: 1.29 1.63 1.83 | macmini (bots): { 62.36 65.36 71.53 }
2026-10-17T13:23:46Z thinkpad: 0.86 1.49 1.78 | macmini (bots): { 68.76 66.51 71.73 }
2026-10-17T13:24:16Z thinkpad: 1.29 1.52 1.78 | macmini (bots): { 77.68 68.77 72.36 }
2026-10-17T13:24:46Z thinkpad: 1.82 1.62 1.80 | macmini (bots): { 74.03 68.88 72.28 }
2026-10-17T13:25:17Z thinkpad: 1.50 1.56 1.78 | macmini (bots): { 71.53 68.76 72.12 }
2026-10-17T13:25:47Z thinkpad: 0.96 1.42 1.72 | macmini (bots): { 74.46 69.85 72.40 }
2026-10-17T13:26:17Z thinkpad: 0.87 1.35 1.69 | macmini (bots): { 80.85 71.79 73.01 }
2026-10-17T13:26:47Z thinkpad: 3.95 2.02 1.90 | macmini (bots): { 74.77 71.31 72.81 }
2026-10-17T13:27:18Z thinkpad: 2.98 1.97 1.89 | macmini (bots): { 70.52 70.52 72.46 }
2026-10-17T13:27:48Z thinkpad: 3.78 2.26 1.99 | macmini (bots): { 67.62 69.91 72.18 }
2026-10-17T13:28:18Z thinkpad: 3.05 2.23 1.99 | macmini (bots): { 72.51 70.83 72.43 }
2026-10-17T13:28:48Z thinkpad: 2.05 2.07 1.94 | macmini (bots): { 79.07 72.64 73.03 }
2026-10-17T13:29:19Z thinkpad: 2.09 2.11 1.96 | macmini (bots): { 90.42 75.94 74.21 }
2026-10-17T13:29:49Z thinkpad: 1.61 1.99 1.93 | macmini (bots): { 102.68 80.15 75.78 }
2026-10-17T13:30:19Z thinkpad: 2.31 2.12 1.97 | macmini (bots): { 111.15 84.19 77.39 }
2026-10-17T13:30:49Z thinkpad: 4.14 2.61 2.14 | macmini (bots): { 115.67 87.82 78.94 }
2026-10-17T13:31:20Z thinkpad: 4.84 2.92 2.26 | macmini (bots): { 122.95 92.31 80.88 }
2026-10-17T13:31:50Z thinkpad: 6.84 3.60 2.51 | macmini (bots): { 136.25 98.41 83.48 }
2026-10-17T13:32:20Z thinkpad: 4.85 3.43 2.49 | macmini (bots): { 128.59 100.39 84.73 }
2026-10-17T13:32:50Z thinkpad: 3.48 3.25 2.46 | macmini (bots): { 111.05 98.73 84.66 }
2026-10-17T13:33:20Z thinkpad: 3.09 3.15 2.45 | macmini (bots): { 102.40 98.03 84.98 }
2026-10-17T13:33:51Z thinkpad: 2.47 2.99 2.42 | macmini (bots): { 94.57 96.54 84.89 }
2026-10-17T13:34:21Z thinkpad: 2.42 2.94 2.42 | macmini (bots): { 86.26 94.36 84.50 }
2026-10-17T13:34:51Z thinkpad: 2.95 3.04 2.47 | macmini (bots): { 76.90 91.56 83.84 }
2026-10-17T13:35:21Z thinkpad: 2.66 2.96 2.46 | macmini (bots): { 65.57 87.42 82.61 }
2026-10-17T13:35:52Z thinkpad: 2.47 2.88 2.45 | macmini (bots): { 64.13 85.05 81.92 }
2026-10-17T13:36:22Z thinkpad: 3.89 3.17 2.56 | macmini (bots): { 58.65 81.74 80.83 }
2026-10-17T13:36:52Z thinkpad: 6.43 3.83 2.80 | macmini (bots): { 66.32 81.32 80.71 }
2026-10-17T13:37:22Z thinkpad: 8.73 4.60 3.09 | macmini (bots): { 67.30 80.14 80.30 }
2026-10-17T13:37:53Z thinkpad: 9.03 5.10 3.30 | macmini (bots): { 63.66 78.24 79.62 }
2026-10-17T13:38:23Z thinkpad: 9.49 5.57 3.52 | macmini (bots): { 61.25 76.23 78.84 }
2026-10-17T13:38:53Z thinkpad: 7.55 5.50 3.56 | macmini (bots): { 62.30 74.88 78.25 }
2026-10-17T13:39:23Z thinkpad: 8.62 5.99 3.79 | macmini (bots): { 57.34 72.56 77.29 }
2026-10-17T13:39:53Z thinkpad: 6.65 5.81 3.80 | macmini (bots): { 51.24 69.62 76.06 }
2026-10-17T13:40:24Z thinkpad: 5.55 5.62 3.80 | macmini (bots): { 48.87 67.26 74.98 }
2026-10-17T13:40:54Z thinkpad: 6.11 5.71 3.89 | macmini (bots): { 51.16 66.08 74.29 }
2026-10-17T13:41:24Z thinkpad: 8.93 6.43 4.19 | macmini (bots): { 48.67 64.00 73.24 }
2026-10-17T13:41:54Z thinkpad: 9.05 6.74 4.36 | macmini (bots): { 46.47 61.96 72.18 }
2026-10-17T13:42:25Z thinkpad: 7.70 6.65 4.41 | macmini (bots): { 44.24 60.05 71.14 }
2026-10-17T13:42:55Z thinkpad: 5.64 6.24 4.34 | macmini (bots): { 49.30 59.83 70.68 }
2026-10-17T13:43:25Z thinkpad: 4.83 6.02 4.33 | macmini (bots): { 47.39 58.24 69.72 }
2026-10-17T13:43:55Z thinkpad: 3.57 5.61 4.25 | macmini (bots): { 47.32 57.23 68.96 }
2026-10-17T13:44:25Z thinkpad: 3.41 5.40 4.23 | macmini (bots): { 47.86 56.49 68.28 }
2026-10-17T13:44:55Z thinkpad: 3.37 5.21 4.20 | macmini (bots): { 50.10 56.19 67.77 }
2026-10-17T13:45:26Z thinkpad: 3.78 5.16 4.22 | macmini (bots): { 51.91 55.98 67.28 }
2026-10-17T13:45:56Z thinkpad: 4.34 5.12 4.23 | macmini (bots): { 49.16 54.85 66.42 }
2026-10-17T13:46:26Z thinkpad: 10.04 6.46 4.71 | macmini (bots): { 45.01 53.33 65.47 }
2026-10-17T13:46:56Z thinkpad: 18.10 8.73 5.52 | macmini (bots): { 46.41 52.87 64.88 }
2026-10-17T13:47:27Z thinkpad: 14.99 8.91 5.68 | macmini (bots): { 51.12 53.21 64.57 }
2026-10-17T13:47:57Z thinkpad: 13.46 9.27 5.92 | macmini (bots): { 53.84 53.81 64.40 }
2026-10-17T13:48:27Z thinkpad: 12.76 9.41 6.07 | macmini (bots): { 51.94 53.27 63.84 }
2026-10-17T13:48:58Z thinkpad: 10.20 9.11 6.08 | macmini (bots): { 50.70 52.83 63.31 }
2026-10-17T13:49:28Z thinkpad: 9.95 9.25 6.23 | macmini (bots): { 51.51 52.89 62.97 }
2026-10-17T13:49:58Z thinkpad: 11.98 9.87 6.53 | macmini (bots): { 50.98 52.73 62.57 }
2026-10-17T13:50:29Z thinkpad: 14.96 10.76 6.94 | macmini (bots): { 49.68 52.31 62.08 }
2026-10-17T13:50:59Z thinkpad: 10.54 10.09 6.83 | macmini (bots): { 53.34 53.01 62.00 }
2026-10-17T13:51:29Z thinkpad: 10.21 10.01 6.91 | macmini (bots): { 51.61 52.69 61.57 }
2026-10-17T13:52:00Z thinkpad: 11.86 10.42 7.14 | macmini (bots): { 48.94 51.89 60.97 }
2026-10-17T13:52:30Z thinkpad: 10.88 10.33 7.22 | macmini (bots): { 48.46 51.44 60.49 }
2026-10-17T13:53:01Z thinkpad: 10.94 10.41 7.35 | macmini (bots): { 54.27 52.41 60.47 }
2026-10-17T13:53:31Z thinkpad: 10.77 10.42 7.45 | macmini (bots): { 57.13 53.30 60.51 }
2026-10-17T13:54:02Z thinkpad: 10.62 10.40 7.53 | macmini (bots): { 53.78 52.84 60.09 }
2026-10-17T13:54:32Z thinkpad: 10.85 10.48 7.66 | macmini (bots): { 52.01 52.51 59.73 }
2026-10-17T13:55:02Z thinkpad: 10.36 10.39 7.73 | macmini (bots): { 53.44 52.83 59.59 }
2026-10-17T13:55:32Z thinkpad: 8.58 9.95 7.67 | macmini (bots): { 58.66 54.12 59.82 }
2026-10-17T13:56:03Z thinkpad: 7.29 9.52 7.60 | macmini (bots): { 60.98 55.05 59.96 }
2026-10-17T13:56:33Z thinkpad: 5.02 8.75 7.40 | macmini (bots): { 65.14 56.71 60.39 }
2026-10-17T13:57:03Z thinkpad: 5.03 8.38 7.32 | macmini (bots): { 57.51 55.73 59.91 }
2026-10-17T13:57:34Z thinkpad: 7.39 8.66 7.45 | macmini (bots): { 52.57 54.73 59.41 }
2026-10-17T13:58:04Z thinkpad: 10.56 9.45 7.76 | macmini (bots): { 53.11 54.60 59.20 }
2026-10-17T13:58:34Z thinkpad: 28.87 14.20 9.41 | macmini (bots): { 54.53 54.93 59.16 }
2026-10-17T13:59:05Z thinkpad: 39.33 17.79 10.73 | macmini (bots): { 62.67 56.86 59.71 }
2026-10-17T13:59:35Z thinkpad: 34.07 18.33 11.12 | macmini (bots): { 61.78 57.22 59.75 }
2026-10-17T14:00:06Z thinkpad: 34.42 19.72 11.80 | macmini (bots): { 53.10 55.55 59.03 }
2026-10-17T14:00:36Z thinkpad: 33.60 20.94 12.46 | macmini (bots): { 48.70 54.23 58.43 }
2026-10-17T14:01:07Z thinkpad: 29.29 20.99 12.74 | macmini (bots): { 50.25 54.00 58.20 }
2026-10-17T14:01:37Z thinkpad: 20.83 19.79 12.61 | macmini (bots): { 50.48 53.74 57.96 }
2026-10-17T14:02:08Z thinkpad: 21.74 20.44 13.11 | macmini (bots): { 48.65 53.04 57.56 }
2026-10-17T14:02:38Z thinkpad: 17.68 19.67 13.09 | macmini (bots): { 54.17 53.85 57.69 }
2026-10-17T14:03:08Z thinkpad: 13.97 18.55 12.93 | macmini (bots): { 53.39 53.84 57.56 }
2026-10-17T14:03:39Z thinkpad: 14.10 18.17 12.98 | macmini (bots): { 50.71 53.09 57.16 }
2026-10-17T14:04:09Z thinkpad: 13.60 17.65 12.97 | macmini (bots): { 52.30 53.28 57.08 }
2026-10-17T14:04:39Z thinkpad: 12.09 16.91 12.88 | macmini (bots): { 51.52 53.03 56.87 }
2026-10-17T14:05:10Z thinkpad: 10.15 15.98 12.69 | macmini (bots): { 54.53 53.57 56.91 }
2026-10-17T14:05:42Z thinkpad: 7.77 14.86 12.42 | macmini (bots): { 63.58 55.55 57.49 }
2026-10-17T14:06:12Z thinkpad: 6.58 13.89 12.18 | macmini (bots): { 75.29 59.41 58.84 }
2026-10-17T14:06:42Z thinkpad: 5.96 13.02 11.94 | macmini (bots): { 78.06 61.64 59.67 }
2026-10-17T14:07:12Z thinkpad: 4.86 12.08 11.66 | macmini (bots): { 72.26 61.85 59.81 }
2026-10-17T14:07:42Z thinkpad: 4.49 11.18 11.38 | macmini (bots): { 67.84 61.73 59.84 }
2026-10-17T14:08:13Z thinkpad: 4.37 10.51 11.15 | macmini (bots): { 64.25 61.45 59.80 }
2026-10-17T14:08:43Z thinkpad: 4.57 9.97 10.94 | macmini (bots): { 57.86 60.14 59.38 }
2026-10-17T14:09:13Z thinkpad: 4.60 9.46 10.74 | macmini (bots): { 60.21 60.39 59.49 }
2026-10-17T14:09:43Z thinkpad: 4.20 8.89 10.51 | macmini (bots): { 57.11 59.57 59.22 }
2026-10-17T14:10:14Z thinkpad: 3.80 8.36 10.28 | macmini (bots): { 66.05 61.48 59.92 }
2026-10-17T14:10:44Z thinkpad: 4.19 8.00 10.10 | macmini (bots): { 78.51 64.91 61.22 }
2026-10-17T14:11:14Z thinkpad: 4.27 7.65 9.92 | macmini (bots): { 74.15 65.33 61.51 }
2026-10-17T14:11:44Z thinkpad: 4.35 7.35 9.74 | macmini (bots): { 62.47 63.48 60.98 }
2026-10-17T14:12:14Z thinkpad: 4.98 7.22 9.62 | macmini (bots): { 56.73 61.86 60.46 }
2026-10-17T14:12:45Z thinkpad: 4.60 6.91 9.44 | macmini (bots): { 57.78 61.49 60.37 }
2026-10-17T14:13:15Z thinkpad: 4.15 6.57 9.25 | macmini (bots): { 56.21 60.71 60.12 }
2026-10-17T14:13:45Z thinkpad: 3.87 6.25 9.05 | macmini (bots): { 54.75 60.00 59.89 }
2026-10-17T14:14:15Z thinkpad: 3.86 6.02 8.89 | macmini (bots): { 52.21 58.88 59.49 }
2026-10-17T14:14:46Z thinkpad: 3.14 5.64 8.67 | macmini (bots): { 58.81 59.59 59.71 }
2026-10-17T14:15:16Z thinkpad: 3.76 5.54 8.54 | macmini (bots): { 64.13 60.86 60.16 }
2026-10-17T14:15:46Z thinkpad: 3.91 5.41 8.40 | macmini (bots): { 59.02 59.97 59.87 }
2026-10-17T14:16:16Z thinkpad: 4.33 5.35 8.28 | macmini (bots): { 64.48 60.97 60.22 }
2026-10-17T14:16:46Z thinkpad: 5.20 5.44 8.21 | macmini (bots): { 79.56 64.98 61.70 }
2026-10-17T14:17:17Z thinkpad: 4.88 5.36 8.10 | macmini (bots): { 94.21 69.99 63.63 }
2026-10-17T14:17:47Z thinkpad: 4.80 5.30 8.00 | macmini (bots): { 85.75 70.51 64.08 }
2026-10-17T14:18:17Z thinkpad: 4.82 5.25 7.89 | macmini (bots): { 82.33 71.25 64.57 }
2026-10-17T14:18:48Z thinkpad: 4.50 5.12 7.75 | macmini (bots): { 77.67 71.11 64.75 }
2026-10-17T14:19:18Z thinkpad: 3.76 4.88 7.58 | macmini (bots): { 77.29 71.67 65.17 }
2026-10-17T14:19:48Z thinkpad: 3.68 4.75 7.45 | macmini (bots): { 71.16 70.71 65.05 }
2026-10-17T14:20:18Z thinkpad: 4.25 4.82 7.39 | macmini (bots): { 66.97 69.63 64.85 }
2026-10-17T14:20:49Z thinkpad: 4.72 4.89 7.33 | macmini (bots): { 59.23 67.50 64.24 }
2026-10-17T14:21:19Z thinkpad: 5.12 4.98 7.28 | macmini (bots): { 52.38 65.10 63.48 }
2026-10-17T14:21:49Z thinkpad: 5.69 5.13 7.26 | macmini (bots): { 59.43 65.53 63.69 }
2026-10-17T14:22:19Z thinkpad: 5.78 5.19 7.21 | macmini (bots): { 62.28 65.65 63.80 }
2026-10-17T14:22:50Z thinkpad: 5.34 5.14 7.13 | macmini (bots): { 57.40 64.12 63.30 }
2026-10-17T14:23:20Z thinkpad: 4.89 5.04 7.03 | macmini (bots): { 58.39 63.61 63.14 }
2026-10-17T14:23:50Z thinkpad: 5.00 5.07 6.98 | macmini (bots): { 64.88 64.63 63.52 }
2026-10-17T14:24:20Z thinkpad: 3.88 4.80 6.83 | macmini (bots): { 80.70 68.30 64.88 }
2026-10-17T14:24:51Z thinkpad: 2.90 4.47 6.65 | macmini (bots): { 94.09 72.75 66.61 }
2026-10-17T14:25:21Z thinkpad: 2.63 4.25 6.51 | macmini (bots): { 93.13 74.91 67.63 }
2026-10-17T14:25:51Z thinkpad: 6.66 5.07 6.71 | macmini (bots): { 72.87 71.72 66.72 }
2026-10-17T14:26:21Z thinkpad: 7.15 5.34 6.74 | macmini (bots): { 67.03 70.37 66.40 }
2026-10-17T14:26:51Z thinkpad: 6.20 5.29 6.68 | macmini (bots): { 58.68 68.08 65.71 }
2026-10-17T14:27:22Z thinkpad: 6.11 5.35 6.66 | macmini (bots): { 55.26 66.28 65.14 }
2026-10-17T14:27:52Z thinkpad: 5.45 5.26 6.59 | macmini (bots): { 56.51 65.74 64.99 }
2026-10-17T14:28:22Z thinkpad: 6.11 5.45 6.61 | macmini (bots): { 52.24 63.77 64.30 }
2026-10-17T14:28:52Z thinkpad: 6.06 5.50 6.59 | macmini (bots): { 55.56 63.38 64.13 }
2026-10-17T14:29:23Z thinkpad: 6.11 5.58 6.58 | macmini (bots): { 59.66 63.63 64.20 }
2026-10-17T14:29:53Z thinkpad: 5.52 5.54 6.53 | macmini (bots): { 63.71 64.30 64.42 }
2026-10-17T14:30:23Z thinkpad: 4.79 5.35 6.43 | macmini (bots): { 69.84 65.61 64.89 }
2026-10-17T14:30:53Z thinkpad: 6.21 5.63 6.49 | macmini (bots): { 62.76 64.46 64.50 }
2026-10-17T14:31:23Z thinkpad: 8.36 6.21 6.66 | macmini (bots): { 53.51 62.09 63.64 }
2026-10-17T14:31:54Z thinkpad: 8.49 6.45 6.72 | macmini (bots): { 52.80 60.98 63.18 }
2026-10-17T14:32:24Z thinkpad: 8.28 6.59 6.77 | macmini (bots): { 48.98 59.20 62.45 }
2026-10-17T14:32:54Z thinkpad: 8.02 6.70 6.79 | macmini (bots): { 48.95 58.30 62.02 }
2026-10-17T14:33:25Z thinkpad: 7.02 6.59 6.76 | macmini (bots): { 51.06 57.70 61.66 }
2026-10-17T14:33:55Z thinkpad: 7.09 6.62 6.76 | macmini (bots): { 70.89 61.81 63.00 }
2026-10-17T14:34:25Z thinkpad: 6.18 6.44 6.69 | macmini (bots): { 69.38 62.46 63.21 }
2026-10-17T14:34:55Z thinkpad: 6.67 6.52 6.72 | macmini (bots): { 76.87 64.84 64.04 }
2026-10-17T14:35:25Z thinkpad: 4.76 6.08 6.56 | macmini (bots): { 86.19 68.04 65.21 }
2026-10-17T14:35:56Z thinkpad: 3.47 5.64 6.40 | macmini (bots): { 101.03 73.28 67.21 }
2026-10-17T14:36:26Z thinkpad: 2.68 5.24 6.24 | macmini (bots): { 106.05 77.09 68.79 }
2026-10-17T14:36:56Z thinkpad: 2.41 4.91 6.10 | macmini (bots): { 107.57 80.20 70.20 }
2026-10-17T14:37:26Z thinkpad: 3.74 4.99 6.08 | macmini (bots): { 101.16 81.38 70.98 }
2026-10-17T14:37:57Z thinkpad: 5.96 5.39 6.18 | macmini (bots): { 86.51 79.61 70.70 }
2026-10-17T14:38:27Z thinkpad: 7.91 5.98 6.35 | macmini (bots): { 81.13 79.06 70.81 }
2026-10-17T14:38:57Z thinkpad: 8.45 6.25 6.43 | macmini (bots): { 79.56 79.01 71.08 }
2026-10-17T14:39:27Z thinkpad: 8.75 6.52 6.52 | macmini (bots): { 71.92 77.22 70.71 }
2026-10-17T14:39:58Z thinkpad: 8.86 6.76 6.60 | macmini (bots): { 71.38 76.58 70.70 }
2026-10-17T14:40:28Z thinkpad: 8.74 6.98 6.68 | macmini (bots): { 71.25 76.12 70.74 }
2026-10-17T14:40:58Z thinkpad: 7.99 6.96 6.68 | macmini (bots): { 70.49 75.51 70.71 }
2026-10-17T14:41:28Z thinkpad: 7.64 7.01 6.70 | macmini (bots): { 68.93 74.58 70.56 }
2026-10-17T14:41:59Z thinkpad: 6.57 6.84 6.66 | macmini (bots): { 68.35 73.75 70.39 }
2026-10-17T14:42:29Z thinkpad: 5.83 6.62 6.59 | macmini (bots): { 63.57 72.20 69.95 }
2026-10-17T14:42:59Z thinkpad: 7.40 6.91 6.69 | macmini (bots): { 56.77 69.69 69.12 }
2026-10-17T14:43:29Z thinkpad: 9.07 7.37 6.85 | macmini (bots): { 49.53 66.71 68.06 }
2026-10-17T14:44:00Z thinkpad: 9.39 7.63 6.96 | macmini (bots): { 43.53 63.68 66.91 }
2026-10-17T14:44:30Z thinkpad: 9.16 7.73 7.01 | macmini (bots): { 41.12 61.23 65.92 }
2026-10-17T14:45:00Z thinkpad: 7.96 7.57 6.98 | macmini (bots): { 40.62 59.23 65.03 }
2026-10-17T14:45:30Z thinkpad: 6.34 7.24 6.89 | macmini (bots): { 38.17 56.80 63.94 }
2026-10-17T14:46:01Z thinkpad: 6.39 7.18 6.88 | macmini (bots): { 40.35 55.54 63.24 }
2026-10-17T14:46:31Z thinkpad: 5.63 6.95 6.82 | macmini (bots): { 48.66 56.25 63.24 }
2026-10-17T14:47:01Z thinkpad: 4.73 6.60 6.70 | macmini (bots): { 46.68 55.20 62.62 }
2026-10-17T14:47:31Z thinkpad: 9.18 7.55 7.02 | macmini (bots): { 41.45 53.19 61.64 }
2026-10-17T14:48:01Z thinkpad: 10.40 8.02 7.19 | macmini (bots): { 40.76 51.87 60.87 }
2026-10-17T14:48:32Z thinkpad: 8.24 7.73 7.12 | macmini (bots): { 44.46 51.67 60.48 }
2026-10-17T14:49:02Z thinkpad: 9.60 8.09 7.26 | macmini (bots): { 38.75 49.66 59.45 }
2026-10-17T14:49:32Z thinkpad: 8.90 8.10 7.29 | macmini (bots): { 34.93 47.72 58.41 }
2026-10-17T14:50:02Z thinkpad: 11.17 8.66 7.50 | macmini (bots): { 35.70 46.68 57.66 }
2026-10-17T14:50:33Z thinkpad: 12.06 9.19 7.72 | macmini (bots): { 43.94 47.43 57.54 }
2026-10-17T14:51:03Z thinkpad: 12.50 9.49 7.87 | macmini (bots): { 40.76 46.36 56.80 }
2026-10-17T14:51:33Z thinkpad: 10.35 9.24 7.83 | macmini (bots): { 44.81 46.69 56.55 }
2026-10-17T14:52:03Z thinkpad: 9.32 9.09 7.83 | macmini (bots): { 54.94 48.97 57.04 }
2026-10-17T14:52:34Z thinkpad: 7.07 8.58 7.70 | macmini (bots): { 54.59 49.54 56.92 }
2026-10-17T14:53:04Z thinkpad: 5.33 8.01 7.54 | macmini (bots): { 67.48 53.27 58.02 }
2026-10-17T14:53:34Z thinkpad: 7.22 8.20 7.62 | macmini (bots): { 57.82 52.33 57.52 }
2026-10-17T14:54:04Z thinkpad: 9.69 8.73 7.81 | macmini (bots): { 46.67 50.20 56.57 }
2026-10-17T14:54:35Z thinkpad: 9.14 8.66 7.82 | macmini (bots): { 49.88 50.61 56.49 }
2026-10-17T14:55:05Z thinkpad: 9.75 8.87 7.91 | macmini (bots): { 46.88 49.95 56.06 }
2026-10-17T14:55:35Z thinkpad: 8.51 8.67 7.88 | macmini (bots): { 42.90 48.80 55.43 }
2026-10-17T14:56:06Z thinkpad: 8.46 8.62 7.89 | macmini (bots): { 49.23 49.72 55.53 }
2026-10-17T14:56:36Z thinkpad: 6.19 8.07 7.73 | macmini (bots): { 54.41 50.86 55.74 }
2026-10-17T14:57:06Z thinkpad: 4.69 7.53 7.56 | macmini (bots): { 60.26 52.69 56.24 }
2026-10-17T14:57:36Z thinkpad: 3.21 6.90 7.35 | macmini (bots): { 67.66 55.21 57.03 }
2026-10-17T14:58:06Z thinkpad: 1.94 6.24 7.11 | macmini (bots): { 75.73 58.41 58.13 }
2026-10-17T14:58:37Z thinkpad: 1.18 5.64 6.89 | macmini (bots): { 74.69 59.90 58.68 }
2026-10-17T14:59:07Z thinkpad: 0.78 5.12 6.67 | macmini (bots): { 77.10 61.82 59.42 }
2026-10-17T14:59:37Z thinkpad: 0.88 4.72 6.49 | macmini (bots): { 87.05 65.59 60.86 }
2026-10-17T15:00:07Z thinkpad: 0.53 4.27 6.28 | macmini (bots): { 78.18 65.51 60.99 }
2026-10-17T15:00:38Z thinkpad: 0.38 3.88 6.09 | macmini (bots): { 79.38 67.00 61.69 }
2026-10-17T15:01:08Z thinkpad: 1.50 3.80 5.99 | macmini (bots): { 78.36 67.86 62.18 }
2026-10-17T15:01:38Z thinkpad: 2.97 3.93 5.95 | macmini (bots): { 74.18 67.90 62.39 }
2026-10-17T15:02:08Z thinkpad: 2.61 3.77 5.83 | macmini (bots): { 68.63 67.00 62.25 }
2026-10-17T15:02:39Z thinkpad: 2.10 3.55 5.69 | macmini (bots): { 69.84 67.50 62.60 }
2026-10-17T15:03:09Z thinkpad: 1.44 3.25 5.53 | macmini (bots): { 67.10 67.06 62.61 }
2026-10-17T15:03:39Z thinkpad: 1.39 3.08 5.40 | macmini (bots): { 60.83 65.50 62.19 }
2026-10-17T15:04:09Z thinkpad: 1.28 2.88 5.26 | macmini (bots): { 75.40 68.61 63.46 }
2026-10-17T15:04:40Z thinkpad: 2.30 2.99 5.22 | macmini (bots): { 106.08 76.54 66.50 }
2026-10-17T15:05:10Z thinkpad: 2.65 3.02 5.15 | macmini (bots): { 111.96 81.09 68.52 }
2026-10-17T15:05:40Z thinkpad: 2.41 2.92 5.05 | macmini (bots): { 104.19 82.08 69.31 }
2026-10-17T15:06:10Z thinkpad: 2.71 2.95 4.99 | macmini (bots): { 116.20 87.04 71.54 }
2026-10-17T15:06:40Z thinkpad: 2.51 2.87 4.90 | macmini (bots): { 136.68 94.24 74.66 }
2026-10-17T15:07:11Z thinkpad: 3.68 3.11 4.91 | macmini (bots): { 132.76 97.43 76.50 }
2026-10-17T15:07:41Z thinkpad: 3.77 3.19 4.88 | macmini (bots): { 132.73 100.97 78.51 }
2026-10-17T15:08:11Z thinkpad: 3.00 3.06 4.79 | macmini (bots): { 116.92 100.33 79.07 }
2026-10-17T15:08:41Z thinkpad: 2.40 2.91 4.68 | macmini (bots): { 109.83 100.07 79.70 }
2026-10-17T15:09:12Z thinkpad: 2.18 2.81 4.59 | macmini (bots): { 113.21 101.71 81.00 }
2026-10-17T15:09:42Z thinkpad: 1.67 2.62 4.47 | macmini (bots): { 131.87 107.11 83.66 }
2026-10-17T15:10:12Z thinkpad: 1.90 2.58 4.39 | macmini (bots): { 133.45 109.92 85.49 }
2026-10-17T15:10:42Z thinkpad: 4.00 3.02 4.48 | macmini (bots): { 130.14 111.67 86.99 }
2026-10-17T15:11:13Z thinkpad: 4.77 3.32 4.54 | macmini (bots): { 118.78 110.28 87.32 }
2026-10-17T15:11:43Z thinkpad: 3.86 3.24 4.47 | macmini (bots): { 124.01 112.36 88.87 }
2026-10-17T15:12:13Z thinkpad: 3.61 3.26 4.44 | macmini (bots): { 122.21 112.94 89.88 }
2026-10-17T15:12:43Z thinkpad: 3.12 3.17 4.36 | macmini (bots): { 111.62 111.10 90.00 }
2026-10-17T15:13:14Z thinkpad: 3.07 3.14 4.31 | macmini (bots): { 122.97 113.95 91.77 }
2026-10-17T15:13:44Z thinkpad: 3.33 3.20 4.30 | macmini (bots): { 102.64 109.89 91.07 }
2026-10-17T15:14:14Z thinkpad: 2.85 3.10 4.23 | macmini (bots): { 94.46 107.15 90.72 }
2026-10-17T15:14:44Z thinkpad: 2.66 3.03 4.17 | macmini (bots): { 94.94 106.07 90.90 }
2026-10-17T15:15:14Z thinkpad: 2.54 2.96 4.11 | macmini (bots): { 94.52 104.93 91.01 }
2026-10-17T15:15:45Z thinkpad: 3.20 3.08 4.11 | macmini (bots): { 84.00 101.47 90.25 }
2026-10-17T15:16:15Z thinkpad: 5.56 3.61 4.25 | macmini (bots): { 83.16 99.76 90.02 }
2026-10-17T15:16:45Z thinkpad: 5.66 3.84 4.30 | macmini (bots): { 69.71 94.66 88.56 }
2026-10-17T15:17:15Z thinkpad: 5.04 3.90 4.31 | macmini (bots): { 59.31 89.78 87.00 }
2026-10-17T15:17:46Z thinkpad: 4.29 3.87 4.29 | macmini (bots): { 58.57 86.47 85.89 }
2026-10-17T15:18:16Z thinkpad: 4.16 3.91 4.29 | macmini (bots): { 56.43 83.29 84.75 }
2026-10-17T15:18:46Z thinkpad: 2.80 3.60 4.18 | macmini (bots): { 50.60 79.22 83.22 }
2026-10-17T15:19:16Z thinkpad: 1.85 3.28 4.05 | macmini (bots): { 54.97 77.50 82.46 }
2026-10-17T15:19:47Z thinkpad: 1.82 3.14 3.98 | macmini (bots): { 56.82 75.80 81.67 }
2026-10-17T15:20:17Z thinkpad: 2.19 3.10 3.94 | macmini (bots): { 59.30 74.46 80.97 }
2026-10-17T15:20:47Z thinkpad: 4.44 3.54 4.06 | macmini (bots): { 55.22 72.11 79.90 }
2026-10-17T15:21:17Z thinkpad: 5.99 4.03 4.21 | macmini (bots): { 49.41 69.01 78.50 }
2026-10-17T15:21:48Z thinkpad: 4.85 3.95 4.18 | macmini (bots): { 46.89 66.58 77.29 }
2026-10-17T15:22:18Z thinkpad: 4.31 3.92 4.16 | macmini (bots): { 45.13 64.31 76.10 }
2026-10-17T15:22:48Z thinkpad: 4.28 3.95 4.16 | macmini (bots): { 51.82 63.98 75.56 }
2026-10-17T15:23:18Z thinkpad: 4.75 4.11 4.21 | macmini (bots): { 50.35 62.51 74.63 }
2026-10-17T15:23:48Z thinkpad: 4.08 4.02 4.17 | macmini (bots): { 44.62 60.01 73.31 }
2026-10-17T15:24:19Z thinkpad: 3.93 4.00 4.16 | macmini (bots): { 46.02 58.72 72.37 }
2026-10-17T15:24:49Z thinkpad: 3.67 3.92 4.13 | macmini (bots): { 47.23 57.90 71.60 }
2026-10-17T15:25:19Z thinkpad: 2.79 3.68 4.04 | macmini (bots): { 50.80 57.78 71.09 }
2026-10-17T15:25:49Z thinkpad: 2.79 3.58 3.99 | macmini (bots): { 47.66 56.35 70.11 }
2026-10-17T15:26:19Z thinkpad: 5.04 3.97 4.11 | macmini (bots): { 45.15 54.84 69.08 }
2026-10-17T15:26:50Z thinkpad: 8.46 4.86 4.40 | macmini (bots): { 43.59 53.51 68.10 }
2026-10-17T15:27:20Z thinkpad: 6.84 4.81 4.39 | macmini (bots): { 42.38 52.39 67.19 }
2026-10-17T15:27:50Z thinkpad: 8.24 5.33 4.58 | macmini (bots): { 49.87 53.02 66.82 }
2026-10-17T15:28:21Z thinkpad: 7.11 5.34 4.61 | macmini (bots): { 61.30 55.60 67.28 }
2026-10-17T15:28:51Z thinkpad: 6.11 5.27 4.61 | macmini (bots): { 67.60 57.74 67.66 }
2026-10-17T15:29:21Z thinkpad: 5.33 5.17 4.59 | macmini (bots): { 63.72 57.79 67.33 }
2026-10-17T15:29:51Z thinkpad: 3.57 4.75 4.47 | macmini (bots): { 60.80 57.62 66.94 }
2026-10-17T15:30:22Z thinkpad: 4.74 4.88 4.52 | macmini (bots): { 59.69 57.64 66.62 }
2026-10-17T15:30:52Z thinkpad: 3.44 4.54 4.42 | macmini (bots): { 66.89 59.49 66.97 }
2026-10-17T15:31:22Z thinkpad: 4.57 4.69 4.47 | macmini (bots): { 83.43 64.16 68.40 }
2026-10-17T15:31:52Z thinkpad: 7.17 5.30 4.68 | macmini (bots): { 77.62 64.77 68.49 }
2026-10-17T15:32:23Z thinkpad: 8.55 5.82 4.88 | macmini (bots): { 74.20 65.09 68.47 }
2026-10-17T15:32:53Z thinkpad: 7.49 5.85 4.92 | macmini (bots): { 82.07 67.70 69.29 }
2026-10-17T15:33:23Z thinkpad: 5.65 5.59 4.86 | macmini (bots): { 79.44 68.47 69.51 }
2026-10-17T15:33:54Z thinkpad: 5.73 5.64 4.91 | macmini (bots): { 78.94 69.26 69.76 }
2026-10-17T15:34:24Z thinkpad: 6.84 5.92 5.02 | macmini (bots): { 76.33 69.71 69.91 }
2026-10-17T15:34:54Z thinkpad: 5.11 5.59 4.94 | macmini (bots): { 86.09 72.63 70.95 }
2026-10-17T15:35:24Z thinkpad: 6.68 5.93 5.08 | macmini (bots): { 72.77 70.70 70.31 }
2026-10-17T15:35:55Z thinkpad: 5.75 5.75 5.04 | macmini (bots): { 71.50 70.49 70.24 }
2026-10-17T15:36:25Z thinkpad: 4.31 5.41 4.95 | macmini (bots): { 80.75 72.67 71.03 }
2026-10-17T15:36:55Z thinkpad: 3.15 5.02 4.83 | macmini (bots): { 89.83 75.59 72.14 }
@@ -1,695 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T14:56:24+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "a9ee56a8b143999c9bc7056850ffcd622a9214aa",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/walk-explore.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 9.51,
"mspt": {
"average": 5.77,
"p50": 5.15,
"p95": 11.04,
"p99": 28.99
},
"mspt_worst_p99": 45.9,
"tps": 20.0,
"rss_mb": {
"mean": 777.78,
"max": 779.8
},
"cpu_percent": 59.02,
"chunks_per_second": 9.94,
"unique_per_second": 0.45,
"forgotten_per_second": 9.99,
"waited_ticks_per_second": 0.0,
"chunks_total": 2763,
"chunks_per_bot": {
"walk": 2763.0
},
"unique_chunks_per_bot": {
"walk": 697.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 15.35,
"mspt": {
"average": 18.84,
"p50": 18.87,
"p95": 29.55,
"p99": 51.32
},
"mspt_worst_p99": 456.4,
"tps": 20.0,
"rss_mb": {
"mean": 3442.06,
"max": 3462.1
},
"cpu_percent": 70.63,
"chunks_per_second": 10.47,
"unique_per_second": 0.47,
"forgotten_per_second": 10.53,
"waited_ticks_per_second": 0.0,
"chunks_total": 2763,
"chunks_per_bot": {
"walk": 2763.0
},
"unique_chunks_per_bot": {
"walk": 697.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 9.31,
"mspt": {
"average": 16.23,
"p50": 15.6,
"p95": 29.32,
"p99": 44.43
},
"mspt_worst_p99": 100.6,
"tps": 20.0,
"rss_mb": {
"mean": 781.84,
"max": 782.6
},
"cpu_percent": 136.59,
"chunks_per_second": 93.82,
"unique_per_second": 3.33,
"forgotten_per_second": 93.61,
"waited_ticks_per_second": 0.0,
"chunks_total": 26481,
"chunks_per_bot": {
"walk": 2648.1
},
"unique_chunks_per_bot": {
"walk": 679.5
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 14.65,
"mspt": {
"average": 18.02,
"p50": 15.22,
"p95": 37.62,
"p99": 54.5
},
"mspt_worst_p99": 73.8,
"tps": 20.0,
"rss_mb": {
"mean": 3408.62,
"max": 3423.6
},
"cpu_percent": 100.57,
"chunks_per_second": 101.12,
"unique_per_second": 4.25,
"forgotten_per_second": 101.17,
"waited_ticks_per_second": 0.0,
"chunks_total": 26763,
"chunks_per_bot": {
"walk": 2676.3
},
"unique_chunks_per_bot": {
"walk": 687.8
},
"resent": 0,
"pulled_back": 7,
"moved_too_quickly": 7,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 9.31,
"mspt": {
"average": 23.98,
"p50": 20.89,
"p95": 49.18,
"p99": 65.87
},
"mspt_worst_p99": 109.8,
"tps": 20.0,
"rss_mb": {
"mean": 786.18,
"max": 786.5
},
"cpu_percent": 159.81,
"chunks_per_second": 235.75,
"unique_per_second": 7.84,
"forgotten_per_second": 235.72,
"waited_ticks_per_second": 0.0,
"chunks_total": 67251,
"chunks_per_bot": {
"walk": 2690.04
},
"unique_chunks_per_bot": {
"walk": 683.92
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 14.95,
"mspt": {
"average": 19.19,
"p50": 17.34,
"p95": 33.84,
"p99": 56.21
},
"mspt_worst_p99": 79.2,
"tps": 20.0,
"rss_mb": {
"mean": 3531.46,
"max": 3582.1
},
"cpu_percent": 153.78,
"chunks_per_second": 252.35,
"unique_per_second": 11.73,
"forgotten_per_second": 251.58,
"waited_ticks_per_second": 0.0,
"chunks_total": 66759,
"chunks_per_bot": {
"walk": 2670.36
},
"unique_chunks_per_bot": {
"walk": 689.76
},
"resent": 0,
"pulled_back": 12,
"moved_too_quickly": 12,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 9.21,
"mspt": {
"average": 35.07,
"p50": 30.55,
"p95": 73.24,
"p99": 99.38
},
"mspt_worst_p99": 117.2,
"tps": 20.0,
"rss_mb": {
"mean": 816.58,
"max": 818.5
},
"cpu_percent": 187.42,
"chunks_per_second": 469.11,
"unique_per_second": 15.92,
"forgotten_per_second": 468.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 136962,
"chunks_per_bot": {
"walk": 2739.24
},
"unique_chunks_per_bot": {
"walk": 685.68
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 15.95,
"mspt": {
"average": 35.23,
"p50": 31.39,
"p95": 60.87,
"p99": 104.2
},
"mspt_worst_p99": 443.7,
"tps": 20.0,
"rss_mb": {
"mean": 3562.57,
"max": 3598.9
},
"cpu_percent": 263.27,
"chunks_per_second": 497.51,
"unique_per_second": 30.67,
"forgotten_per_second": 495.7,
"waited_ticks_per_second": 0.0,
"chunks_total": 131116,
"chunks_per_bot": {
"walk": 2622.32
},
"unique_chunks_per_bot": {
"walk": 687.78
},
"resent": 0,
"pulled_back": 96,
"moved_too_quickly": 96,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 9.21,
"mspt": {
"average": 104.8,
"p50": 100.6,
"p95": 158.8,
"p99": 198.61
},
"mspt_worst_p99": 259.8,
"tps": 9.54,
"rss_mb": {
"mean": 850.05,
"max": 851.2
},
"cpu_percent": 198.95,
"chunks_per_second": 371.72,
"unique_per_second": 26.27,
"forgotten_per_second": 365.57,
"waited_ticks_per_second": 0.0,
"chunks_total": 144465,
"chunks_per_bot": {
"walk": 1444.65
},
"unique_chunks_per_bot": {
"walk": 571.76
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 34
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 15.25,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.41,
"mspt": {
"average": 11.42,
"p50": 10.15,
"p95": 24.66,
"p99": 37.27
},
"mspt_worst_p99": 60.3,
"tps": 20.0,
"rss_mb": {
"mean": 838.61,
"max": 888.0
},
"cpu_percent": 326.85,
"chunks_per_second": 41.83,
"unique_per_second": 36.39,
"forgotten_per_second": 41.75,
"waited_ticks_per_second": 0.0,
"chunks_total": 9845,
"chunks_per_bot": {
"explore": 9845.0
},
"unique_chunks_per_bot": {
"explore": 8718.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.44,
"mspt": {
"average": 9.83,
"p50": 7.72,
"p95": 20.36,
"p99": 84.65
},
"mspt_worst_p99": 385.9,
"tps": 20.0,
"rss_mb": {
"mean": 3598.02,
"max": 3658.9
},
"cpu_percent": 244.86,
"chunks_per_second": 35.28,
"unique_per_second": 33.34,
"forgotten_per_second": 35.13,
"waited_ticks_per_second": 0.0,
"chunks_total": 7881,
"chunks_per_bot": {
"explore": 7881.0
},
"unique_chunks_per_bot": {
"explore": 7540.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.52,
"mspt": {
"average": 23.54,
"p50": 21.18,
"p95": 43.4,
"p99": 60.85
},
"mspt_worst_p99": 109.1,
"tps": 20.0,
"rss_mb": {
"mean": 1139.51,
"max": 1301.0
},
"cpu_percent": 331.74,
"chunks_per_second": 43.86,
"unique_per_second": 43.09,
"forgotten_per_second": 43.82,
"waited_ticks_per_second": 172.35,
"chunks_total": 14140,
"chunks_per_bot": {
"explore": 1414.0
},
"unique_chunks_per_bot": {
"explore": 1395.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.78,
"mspt": {
"average": 16.42,
"p50": 14.01,
"p95": 30.29,
"p99": 96.17
},
"mspt_worst_p99": 1144.3,
"tps": 20.0,
"rss_mb": {
"mean": 3964.49,
"max": 4248.3
},
"cpu_percent": 256.86,
"chunks_per_second": 25.43,
"unique_per_second": 25.33,
"forgotten_per_second": 25.96,
"waited_ticks_per_second": 76.05,
"chunks_total": 7991,
"chunks_per_bot": {
"explore": 799.1
},
"unique_chunks_per_bot": {
"explore": 797.7
},
"resent": 0,
"pulled_back": 11028,
"moved_too_quickly": 17,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 36.87,
"p50": 33.92,
"p95": 63.19,
"p99": 84.17
},
"mspt_worst_p99": 124.2,
"tps": 20.0,
"rss_mb": {
"mean": 1236.2,
"max": 1438.4
},
"cpu_percent": 345.03,
"chunks_per_second": 91.07,
"unique_per_second": 88.7,
"forgotten_per_second": 92.51,
"waited_ticks_per_second": 443.88,
"chunks_total": 30035,
"chunks_per_bot": {
"explore": 1201.4
},
"unique_chunks_per_bot": {
"explore": 1176.84
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.55,
"mspt": {
"average": 24.31,
"p50": 21.76,
"p95": 36.55,
"p99": 155.88
},
"mspt_worst_p99": 1535.8,
"tps": 20.0,
"rss_mb": {
"mean": 4047.76,
"max": 4459.2
},
"cpu_percent": 305.24,
"chunks_per_second": 37.94,
"unique_per_second": 37.82,
"forgotten_per_second": 38.87,
"waited_ticks_per_second": 261.79,
"chunks_total": 16604,
"chunks_per_bot": {
"explore": 664.16
},
"unique_chunks_per_bot": {
"explore": 660.92
},
"resent": 0,
"pulled_back": 33246,
"moved_too_quickly": 20,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.21,
"mspt": {
"average": 60.06,
"p50": 57.31,
"p95": 88.38,
"p99": 112.89
},
"mspt_worst_p99": 172.4,
"tps": 16.65,
"rss_mb": {
"mean": 1192.47,
"max": 1372.5
},
"cpu_percent": 362.42,
"chunks_per_second": 157.28,
"unique_per_second": 154.2,
"forgotten_per_second": 156.07,
"waited_ticks_per_second": 890.09,
"chunks_total": 54551,
"chunks_per_bot": {
"explore": 1091.02
},
"unique_chunks_per_bot": {
"explore": 1078.5
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 15.64,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 9.21,
"mspt": {
"average": 124.25,
"p50": 120.1,
"p95": 168.33,
"p99": 204.64
},
"mspt_worst_p99": 262.2,
"tps": 8.05,
"rss_mb": {
"mean": 1283.26,
"max": 1418.6
},
"cpu_percent": 334.14,
"chunks_per_second": 185.47,
"unique_per_second": 184.54,
"forgotten_per_second": 179.79,
"waited_ticks_per_second": 1864.45,
"chunks_total": 78556,
"chunks_per_bot": {
"explore": 785.56
},
"unique_chunks_per_bot": {
"explore": 784.88
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 15.44,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -1,341 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T18:12:19+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "7bbbf6e97fb53683eae0a01ce2c134e53cb649a7",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/explore-equal-rate.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.42,
"mspt": {
"average": 3.16,
"p50": 2.98,
"p95": 5.78,
"p99": 12.17
},
"mspt_worst_p99": 38.1,
"tps": 20.0,
"rss_mb": {
"mean": 759.26,
"max": 769.5
},
"cpu_percent": 21.74,
"chunks_per_second": 1.64,
"unique_per_second": 1.27,
"forgotten_per_second": 1.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 841,
"chunks_per_bot": {
"explore": 841.0
},
"unique_chunks_per_bot": {
"explore": 781.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 17.17,
"mspt": {
"average": 16.82,
"p50": 16.67,
"p95": 27.3,
"p99": 52.16
},
"mspt_worst_p99": 453.4,
"tps": 20.0,
"rss_mb": {
"mean": 3373.16,
"max": 3405.9
},
"cpu_percent": 69.22,
"chunks_per_second": 1.66,
"unique_per_second": 1.35,
"forgotten_per_second": 1.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 838,
"chunks_per_bot": {
"explore": 838.0
},
"unique_chunks_per_bot": {
"explore": 779.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.82,
"mspt": {
"average": 9.83,
"p50": 8.65,
"p95": 19.01,
"p99": 32.9
},
"mspt_worst_p99": 64.0,
"tps": 20.0,
"rss_mb": {
"mean": 861.49,
"max": 922.2
},
"cpu_percent": 77.43,
"chunks_per_second": 13.71,
"unique_per_second": 12.31,
"forgotten_per_second": 13.59,
"waited_ticks_per_second": 0.0,
"chunks_total": 7831,
"chunks_per_bot": {
"explore": 783.1
},
"unique_chunks_per_bot": {
"explore": 757.1
},
"resent": 0,
"pulled_back": 1051,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.06,
"mspt": {
"average": 24.38,
"p50": 21.55,
"p95": 39.58,
"p99": 66.64
},
"mspt_worst_p99": 107.1,
"tps": 20.0,
"rss_mb": {
"mean": 3569.41,
"max": 3685.1
},
"cpu_percent": 132.09,
"chunks_per_second": 13.54,
"unique_per_second": 12.32,
"forgotten_per_second": 13.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 7753,
"chunks_per_bot": {
"explore": 775.3
},
"unique_chunks_per_bot": {
"explore": 749.2
},
"resent": 0,
"pulled_back": 1027,
"moved_too_quickly": 6,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 16.87,
"p50": 15.4,
"p95": 30.88,
"p99": 46.73
},
"mspt_worst_p99": 84.9,
"tps": 20.0,
"rss_mb": {
"mean": 887.11,
"max": 945.5
},
"cpu_percent": 111.2,
"chunks_per_second": 33.51,
"unique_per_second": 29.92,
"forgotten_per_second": 33.33,
"waited_ticks_per_second": 0.0,
"chunks_total": 19718,
"chunks_per_bot": {
"explore": 788.72
},
"unique_chunks_per_bot": {
"explore": 759.08
},
"resent": 0,
"pulled_back": 3770,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.94,
"mspt": {
"average": 30.29,
"p50": 28.25,
"p95": 43.47,
"p99": 68.56
},
"mspt_worst_p99": 165.2,
"tps": 20.0,
"rss_mb": {
"mean": 3608.97,
"max": 3732.0
},
"cpu_percent": 176.28,
"chunks_per_second": 32.98,
"unique_per_second": 29.29,
"forgotten_per_second": 32.84,
"waited_ticks_per_second": 0.0,
"chunks_total": 19442,
"chunks_per_bot": {
"explore": 777.68
},
"unique_chunks_per_bot": {
"explore": 748.24
},
"resent": 0,
"pulled_back": 5139,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.41,
"mspt": {
"average": 23.57,
"p50": 20.43,
"p95": 47.53,
"p99": 67.23
},
"mspt_worst_p99": 106.2,
"tps": 20.0,
"rss_mb": {
"mean": 924.41,
"max": 984.3
},
"cpu_percent": 127.14,
"chunks_per_second": 66.56,
"unique_per_second": 59.42,
"forgotten_per_second": 66.43,
"waited_ticks_per_second": 0.0,
"chunks_total": 39950,
"chunks_per_bot": {
"explore": 799.0
},
"unique_chunks_per_bot": {
"explore": 767.56
},
"resent": 0,
"pulled_back": 7434,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 15.04,
"mspt": {
"average": 42.44,
"p50": 40.71,
"p95": 54.84,
"p99": 94.23
},
"mspt_worst_p99": 212.3,
"tps": 20.0,
"rss_mb": {
"mean": 3667.85,
"max": 3780.8
},
"cpu_percent": 200.85,
"chunks_per_second": 65.17,
"unique_per_second": 58.11,
"forgotten_per_second": 65.22,
"waited_ticks_per_second": 0.0,
"chunks_total": 39772,
"chunks_per_bot": {
"explore": 795.44
},
"unique_chunks_per_bot": {
"explore": 764.02
},
"resent": 0,
"pulled_back": 10604,
"moved_too_quickly": 38,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
}
]
}
-279
View File
@@ -1,279 +0,0 @@
2026-10-17T15:51:54Z thinkpad: 1.30 2.47 3.85 | macmini (bots): { 67.44 76.34 77.44 }
2026-10-17T15:52:25Z thinkpad: 2.07 2.54 3.83 | macmini (bots): { 64.78 74.76 76.82 }
2026-10-17T15:52:55Z thinkpad: 2.08 2.51 3.78 | macmini (bots): { 62.50 73.29 76.22 }
2026-10-17T15:53:25Z thinkpad: 1.46 2.31 3.67 | macmini (bots): { 63.70 72.67 75.90 }
2026-10-17T15:53:55Z thinkpad: 1.12 2.15 3.58 | macmini (bots): { 69.06 73.27 76.01 }
2026-10-17T15:54:26Z thinkpad: 1.34 2.09 3.51 | macmini (bots): { 72.55 73.63 76.04 }
2026-10-17T15:54:56Z thinkpad: 1.11 1.97 3.42 | macmini (bots): { 77.53 74.77 76.37 }
2026-10-17T15:55:26Z thinkpad: 1.02 1.87 3.34 | macmini (bots): { 77.61 74.90 76.36 }
2026-10-17T15:55:56Z thinkpad: 1.09 1.81 3.27 | macmini (bots): { 71.56 73.69 75.86 }
2026-10-17T15:56:26Z thinkpad: 2.23 1.99 3.29 | macmini (bots): { 65.21 71.93 75.15 }
2026-10-17T15:56:57Z thinkpad: 2.60 2.12 3.29 | macmini (bots): { 58.16 69.40 74.10 }
2026-10-17T15:57:27Z thinkpad: 2.57 2.18 3.27 | macmini (bots): { 57.54 68.26 73.52 }
2026-10-17T15:57:57Z thinkpad: 2.37 2.17 3.23 | macmini (bots): { 56.15 66.85 72.83 }
2026-10-17T15:58:27Z thinkpad: 1.90 2.08 3.17 | macmini (bots): { 59.28 66.57 72.52 }
2026-10-17T15:58:58Z thinkpad: 1.71 2.02 3.11 | macmini (bots): { 55.64 64.94 71.72 }
2026-10-17T15:59:28Z thinkpad: 1.66 1.97 3.06 | macmini (bots): { 66.06 66.49 72.04 }
2026-10-17T15:59:58Z thinkpad: 1.18 1.83 2.98 | macmini (bots): { 78.15 69.43 72.92 }
2026-10-17T16:00:28Z thinkpad: 0.83 1.68 2.89 | macmini (bots): { 70.77 68.57 72.49 }
2026-10-17T16:00:58Z thinkpad: 1.27 1.70 2.86 | macmini (bots): { 71.95 68.97 72.49 }
2026-10-17T16:01:29Z thinkpad: 1.85 1.79 2.85 | macmini (bots): { 83.43 72.22 73.56 }
2026-10-17T16:01:59Z thinkpad: 2.51 1.95 2.86 | macmini (bots): { 70.49 70.18 72.77 }
2026-10-17T16:02:29Z thinkpad: 1.98 1.87 2.81 | macmini (bots): { 64.00 68.64 72.12 }
2026-10-17T16:02:59Z thinkpad: 1.77 1.84 2.77 | macmini (bots): { 59.65 67.09 71.44 }
2026-10-17T16:03:30Z thinkpad: 4.04 2.35 2.91 | macmini (bots): { 62.67 67.15 71.31 }
2026-10-17T16:04:00Z thinkpad: 4.94 2.72 3.01 | macmini (bots): { 67.63 67.82 71.40 }
2026-10-17T16:04:30Z thinkpad: 4.22 2.78 3.02 | macmini (bots): { 70.41 68.39 71.48 }
2026-10-17T16:05:00Z thinkpad: 3.81 2.80 3.02 | macmini (bots): { 66.59 67.83 71.18 }
2026-10-17T16:05:31Z thinkpad: 3.20 2.75 3.00 | macmini (bots): { 72.53 69.01 71.48 }
2026-10-17T16:06:01Z thinkpad: 4.22 3.04 3.08 | macmini (bots): { 71.30 69.27 71.50 }
2026-10-17T16:06:31Z thinkpad: 5.43 3.47 3.23 | macmini (bots): { 68.37 68.63 71.19 }
2026-10-17T16:07:01Z thinkpad: 4.35 3.42 3.22 | macmini (bots): { 60.20 66.68 70.39 }
2026-10-17T16:07:31Z thinkpad: 3.47 3.31 3.19 | macmini (bots): { 58.03 65.51 69.84 }
2026-10-17T16:08:02Z thinkpad: 2.30 3.04 3.11 | macmini (bots): { 59.05 65.13 69.55 }
2026-10-17T16:08:32Z thinkpad: 1.88 2.86 3.04 | macmini (bots): { 51.77 62.75 68.53 }
2026-10-17T16:09:02Z thinkpad: 1.38 2.65 2.97 | macmini (bots): { 49.05 61.01 67.70 }
2026-10-17T16:09:32Z thinkpad: 3.10 2.95 3.06 | macmini (bots): { 48.91 59.82 67.03 }
2026-10-17T16:10:02Z thinkpad: 2.91 2.91 3.04 | macmini (bots): { 47.56 58.44 66.25 }
2026-10-17T16:10:33Z thinkpad: 2.58 2.83 3.01 | macmini (bots): { 49.23 57.69 65.70 }
2026-10-17T16:11:03Z thinkpad: 8.38 4.15 3.44 | macmini (bots): { 44.16 55.72 64.71 }
2026-10-17T16:11:33Z thinkpad: 9.94 4.92 3.72 | macmini (bots): { 44.50 54.71 64.03 }
2026-10-17T16:12:03Z thinkpad: 10.08 5.42 3.92 | macmini (bots): { 42.15 53.21 63.17 }
2026-10-17T16:12:33Z thinkpad: 6.79 5.06 3.85 | macmini (bots): { 36.13 50.73 61.92 }
2026-10-17T16:13:04Z thinkpad: 4.61 4.72 3.77 | macmini (bots): { 41.28 50.64 61.51 }
2026-10-17T16:13:34Z thinkpad: 3.81 4.50 3.73 | macmini (bots): { 42.92 50.15 60.95 }
2026-10-17T16:14:04Z thinkpad: 3.39 4.35 3.71 | macmini (bots): { 41.56 49.16 60.22 }
2026-10-17T16:14:35Z thinkpad: 7.53 5.17 4.00 | macmini (bots): { 47.48 49.82 60.07 }
2026-10-17T16:15:05Z thinkpad: 9.73 6.01 4.32 | macmini (bots): { 51.43 50.41 59.92 }
2026-10-17T16:15:35Z thinkpad: 8.07 5.94 4.35 | macmini (bots): { 69.31 54.81 61.18 }
2026-10-17T16:16:05Z thinkpad: 9.27 6.45 4.57 | macmini (bots): { 71.85 56.92 61.73 }
2026-10-17T16:16:35Z thinkpad: 8.69 6.59 4.68 | macmini (bots): { 64.77 56.72 61.50 }
2026-10-17T16:17:06Z thinkpad: 6.11 6.17 4.60 | macmini (bots): { 57.92 55.86 61.02 }
2026-10-17T16:17:36Z thinkpad: 6.79 6.33 4.70 | macmini (bots): { 51.44 54.51 60.36 }
2026-10-17T16:18:06Z thinkpad: 5.16 5.99 4.64 | macmini (bots): { 51.33 54.23 60.05 }
2026-10-17T16:18:36Z thinkpad: 5.30 5.93 4.66 | macmini (bots): { 55.22 54.87 60.08 }
2026-10-17T16:19:07Z thinkpad: 4.78 5.72 4.63 | macmini (bots): { 51.62 53.94 59.56 }
2026-10-17T16:19:37Z thinkpad: 5.25 5.78 4.69 | macmini (bots): { 48.77 53.04 59.04 }
2026-10-17T16:20:07Z thinkpad: 4.63 5.60 4.67 | macmini (bots): { 54.59 53.92 59.14 }
2026-10-17T16:20:37Z thinkpad: 5.46 5.67 4.72 | macmini (bots): { 59.07 55.03 59.36 }
2026-10-17T16:21:07Z thinkpad: 8.07 6.19 4.91 | macmini (bots): { 52.53 53.83 58.77 }
2026-10-17T16:21:38Z thinkpad: 15.01 8.05 5.57 | macmini (bots): { 48.85 52.78 58.19 }
2026-10-17T16:22:08Z thinkpad: 19.08 9.69 6.20 | macmini (bots): { 54.73 53.61 58.29 }
2026-10-17T16:22:39Z thinkpad: 27.72 12.63 7.29 | macmini (bots): { 54.23 53.61 58.13 }
2026-10-17T16:23:09Z thinkpad: 23.10 12.88 7.53 | macmini (bots): { 51.63 53.06 57.77 }
2026-10-17T16:23:39Z thinkpad: 20.51 13.53 7.96 | macmini (bots): { 45.29 51.38 57.00 }
2026-10-17T16:24:10Z thinkpad: 12.70 12.30 7.72 | macmini (bots): { 45.61 50.91 56.63 }
2026-10-17T16:24:40Z thinkpad: 8.67 11.35 7.55 | macmini (bots): { 45.18 50.35 56.23 }
2026-10-17T16:25:10Z thinkpad: 6.35 10.53 7.40 | macmini (bots): { 41.10 48.88 55.50 }
2026-10-17T16:25:40Z thinkpad: 6.26 10.16 7.38 | macmini (bots): { 49.86 50.24 55.76 }
2026-10-17T16:26:11Z thinkpad: 7.71 10.11 7.45 | macmini (bots): { 53.93 51.11 55.88 }
2026-10-17T16:26:41Z thinkpad: 9.50 10.32 7.61 | macmini (bots): { 47.61 49.86 55.26 }
2026-10-17T16:27:11Z thinkpad: 10.26 10.39 7.72 | macmini (bots): { 46.43 49.36 54.89 }
2026-10-17T16:27:42Z thinkpad: 10.61 10.48 7.83 | macmini (bots): { 48.88 49.66 54.80 }
2026-10-17T16:28:12Z thinkpad: 10.39 10.45 7.91 | macmini (bots): { 58.42 51.74 55.37 }
2026-10-17T16:28:42Z thinkpad: 10.36 10.44 7.99 | macmini (bots): { 59.59 52.67 55.58 }
2026-10-17T16:29:13Z thinkpad: 9.63 10.25 8.00 | macmini (bots): { 55.30 52.38 55.38 }
2026-10-17T16:29:43Z thinkpad: 10.33 10.37 8.12 | macmini (bots): { 57.79 53.13 55.52 }
2026-10-17T16:30:13Z thinkpad: 9.68 10.18 8.12 | macmini (bots): { 57.83 53.60 55.61 }
2026-10-17T16:30:43Z thinkpad: 9.72 10.17 8.19 | macmini (bots): { 78.94 59.18 57.56 }
2026-10-17T16:31:14Z thinkpad: 6.60 9.36 7.98 | macmini (bots): { 87.29 62.98 58.99 }
2026-10-17T16:31:44Z thinkpad: 6.77 9.08 7.93 | macmini (bots): { 81.82 64.16 59.57 }
2026-10-17T16:32:14Z thinkpad: 13.15 10.41 8.41 | macmini (bots): { 81.20 65.92 60.37 }
2026-10-17T16:32:45Z thinkpad: 27.80 14.12 9.71 | macmini (bots): { 75.41 66.22 60.69 }
2026-10-17T16:33:15Z thinkpad: 44.59 19.25 11.54 | macmini (bots): { 66.92 65.08 60.47 }
2026-10-17T16:33:46Z thinkpad: 44.87 21.89 12.67 | macmini (bots): { 61.47 63.94 60.22 }
2026-10-17T16:34:16Z thinkpad: 38.98 22.67 13.23 | macmini (bots): { 62.13 63.78 60.28 }
2026-10-17T16:34:47Z thinkpad: 47.64 26.08 14.65 | macmini (bots): { 81.23 68.06 61.94 }
2026-10-17T16:35:17Z thinkpad: 47.83 28.02 15.65 | macmini (bots): { 116.14 78.43 65.98 }
2026-10-17T16:35:48Z thinkpad: 40.43 28.18 16.10 | macmini (bots): { 125.61 84.41 68.58 }
2026-10-17T16:36:19Z thinkpad: 41.34 29.71 17.00 | macmini (bots): { 143.61 92.86 72.20 }
2026-10-17T16:36:49Z thinkpad: 34.40 29.19 17.29 | macmini (bots): { 156.05 100.88 75.83 }
2026-10-17T16:37:20Z thinkpad: 25.34 27.65 17.17 | macmini (bots): { 151.90 105.26 78.29 }
2026-10-17T16:37:50Z thinkpad: 18.36 25.71 16.85 | macmini (bots): { 141.10 107.25 79.96 }
2026-10-17T16:38:20Z thinkpad: 16.19 24.47 16.72 | macmini (bots): { 131.21 107.92 81.13 }
2026-10-17T16:38:51Z thinkpad: 13.36 23.00 16.48 | macmini (bots): { 122.08 107.78 82.00 }
2026-10-17T16:39:21Z thinkpad: 11.27 21.57 16.21 | macmini (bots): { 107.07 105.50 82.07 }
2026-10-17T16:39:51Z thinkpad: 11.19 20.56 16.04 | macmini (bots): { 96.38 103.12 82.02 }
2026-10-17T16:40:22Z thinkpad: 10.06 19.37 15.79 | macmini (bots): { 89.06 100.79 81.91 }
2026-10-17T16:40:52Z thinkpad: 7.38 17.86 15.40 | macmini (bots): { 79.35 97.41 81.34 }
2026-10-17T16:41:23Z thinkpad: 6.18 16.57 15.05 | macmini (bots): { 81.70 96.20 81.46 }
2026-10-17T16:41:53Z thinkpad: 5.56 15.44 14.72 | macmini (bots): { 86.51 95.88 81.84 }
2026-10-17T16:42:23Z thinkpad: 4.90 14.33 14.37 | macmini (bots): { 99.20 97.69 83.05 }
2026-10-17T16:42:53Z thinkpad: 5.01 13.47 14.08 | macmini (bots): { 116.29 101.87 85.07 }
2026-10-17T16:43:24Z thinkpad: 4.34 12.50 13.74 | macmini (bots): { 131.32 106.77 87.42 }
2026-10-17T16:43:54Z thinkpad: 4.08 11.65 13.42 | macmini (bots): { 155.44 114.77 90.98 }
2026-10-17T16:44:24Z thinkpad: 4.00 10.90 13.11 | macmini (bots): { 167.86 121.62 94.28 }
2026-10-17T16:44:54Z thinkpad: 3.98 10.13 12.77 | macmini (bots): { 160.36 124.31 96.20 }
2026-10-17T16:45:25Z thinkpad: 4.20 9.59 12.50 | macmini (bots): { 163.23 128.59 98.74 }
2026-10-17T16:45:55Z thinkpad: 4.54 9.15 12.27 | macmini (bots): { 169.93 133.73 101.65 }
2026-10-17T16:46:25Z thinkpad: 4.44 8.69 12.01 | macmini (bots): { 162.40 135.22 103.29 }
2026-10-17T16:46:55Z thinkpad: 4.06 8.20 11.74 | macmini (bots): { 164.16 138.31 105.52 }
2026-10-17T16:47:26Z thinkpad: 3.96 7.78 11.49 | macmini (bots): { 157.92 139.31 107.02 }
2026-10-17T16:47:56Z thinkpad: 3.71 7.35 11.23 | macmini (bots): { 155.72 140.55 108.59 }
2026-10-17T16:48:26Z thinkpad: 4.93 7.32 11.09 | macmini (bots): { 153.39 141.52 110.05 }
2026-10-17T16:48:56Z thinkpad: 4.61 7.03 10.88 | macmini (bots): { 137.54 138.87 110.18 }
2026-10-17T16:49:27Z thinkpad: 4.01 6.65 10.63 | macmini (bots): { 136.53 138.36 110.98 }
2026-10-17T16:49:57Z thinkpad: 3.19 6.19 10.35 | macmini (bots): { 141.12 139.18 112.22 }
2026-10-17T16:50:27Z thinkpad: 5.31 6.43 10.30 | macmini (bots): { 122.76 135.27 111.75 }
2026-10-17T16:50:57Z thinkpad: 5.95 6.48 10.19 | macmini (bots): { 114.26 132.02 111.39 }
2026-10-17T16:51:28Z thinkpad: 5.76 6.41 10.05 | macmini (bots): { 116.04 130.87 111.69 }
2026-10-17T16:51:58Z thinkpad: 5.11 6.18 9.86 | macmini (bots): { 115.06 129.17 111.74 }
2026-10-17T16:52:28Z thinkpad: 5.09 6.07 9.70 | macmini (bots): { 109.91 126.59 111.41 }
2026-10-17T16:52:58Z thinkpad: 4.74 5.90 9.53 | macmini (bots): { 101.90 123.11 110.67 }
2026-10-17T16:53:29Z thinkpad: 4.89 5.84 9.39 | macmini (bots): { 83.29 116.38 108.74 }
2026-10-17T16:53:59Z thinkpad: 5.10 5.82 9.27 | macmini (bots): { 119.16 122.10 111.09 }
2026-10-17T16:54:29Z thinkpad: 4.57 5.67 9.10 | macmini (bots): { 114.87 120.76 110.98 }
2026-10-17T16:55:00Z thinkpad: 4.74 5.58 8.96 | macmini (bots): { 129.84 123.74 112.39 }
2026-10-17T16:55:30Z thinkpad: 6.40 5.90 8.96 | macmini (bots): { 100.10 117.29 110.46 }
2026-10-17T16:56:00Z thinkpad: 6.96 6.08 8.92 | macmini (bots): { 80.18 110.75 108.32 }
2026-10-17T16:56:30Z thinkpad: 6.07 5.95 8.79 | macmini (bots): { 74.41 106.39 106.82 }
2026-10-17T16:57:01Z thinkpad: 6.63 6.13 8.75 | macmini (bots): { 81.71 104.99 106.29 }
2026-10-17T16:57:31Z thinkpad: 6.39 6.14 8.68 | macmini (bots): { 101.10 107.55 107.18 }
2026-10-17T16:58:01Z thinkpad: 6.32 6.16 8.60 | macmini (bots): { 94.45 105.66 106.52 }
2026-10-17T16:58:31Z thinkpad: 5.19 5.90 8.44 | macmini (bots): { 78.76 100.83 104.75 }
2026-10-17T16:59:02Z thinkpad: 5.24 5.83 8.33 | macmini (bots): { 78.19 98.42 103.73 }
2026-10-17T16:59:32Z thinkpad: 3.95 5.48 8.13 | macmini (bots): { 77.57 96.42 102.82 }
2026-10-17T17:00:02Z thinkpad: 3.12 5.13 7.93 | macmini (bots): { 85.23 96.29 102.54 }
2026-10-17T17:00:32Z thinkpad: 3.17 4.93 7.77 | macmini (bots): { 99.84 99.07 103.35 }
2026-10-17T17:01:02Z thinkpad: 6.09 5.50 7.88 | macmini (bots): { 80.56 94.49 101.54 }
2026-10-17T17:01:33Z thinkpad: 9.23 6.29 8.06 | macmini (bots): { 79.70 93.03 100.77 }
2026-10-17T17:02:03Z thinkpad: 9.09 6.54 8.09 | macmini (bots): { 76.63 90.94 99.74 }
2026-10-17T17:02:33Z thinkpad: 7.86 6.52 8.04 | macmini (bots): { 93.08 93.50 100.36 }
2026-10-17T17:03:03Z thinkpad: 7.81 6.67 8.04 | macmini (bots): { 83.14 91.09 99.25 }
2026-10-17T17:03:34Z thinkpad: 6.69 6.51 7.94 | macmini (bots): { 79.37 89.64 98.45 }
2026-10-17T17:04:04Z thinkpad: 5.63 6.27 7.81 | macmini (bots): { 73.68 87.23 97.27 }
2026-10-17T17:04:34Z thinkpad: 5.75 6.21 7.75 | macmini (bots): { 75.89 86.23 96.54 }
2026-10-17T17:05:04Z thinkpad: 5.05 6.01 7.63 | macmini (bots): { 79.29 86.13 96.10 }
2026-10-17T17:05:35Z thinkpad: 3.88 5.62 7.44 | macmini (bots): { 76.07 84.79 95.28 }
2026-10-17T17:06:05Z thinkpad: 6.47 6.07 7.53 | macmini (bots): { 70.59 82.62 94.13 }
2026-10-17T17:06:35Z thinkpad: 8.20 6.53 7.63 | macmini (bots): { 69.09 81.18 93.21 }
2026-10-17T17:07:05Z thinkpad: 7.59 6.54 7.60 | macmini (bots): { 77.55 82.11 93.13 }
2026-10-17T17:07:36Z thinkpad: 6.93 6.49 7.55 | macmini (bots): { 84.23 83.41 93.22 }
2026-10-17T17:08:06Z thinkpad: 7.21 6.61 7.56 | macmini (bots): { 84.31 83.68 92.99 }
2026-10-17T17:08:36Z thinkpad: 6.73 6.57 7.51 | macmini (bots): { 81.04 82.99 92.42 }
2026-10-17T17:09:06Z thinkpad: 6.12 6.43 7.44 | macmini (bots): { 86.55 83.77 92.35 }
2026-10-17T17:09:36Z thinkpad: 6.77 6.58 7.46 | macmini (bots): { 95.19 86.31 92.99 }
2026-10-17T17:10:07Z thinkpad: 7.13 6.68 7.46 | macmini (bots): { 111.30 91.11 94.50 }
2026-10-17T17:10:37Z thinkpad: 5.09 6.23 7.29 | macmini (bots): { 118.51 95.17 95.87 }
2026-10-17T17:11:07Z thinkpad: 4.00 5.86 7.13 | macmini (bots): { 121.12 98.01 96.88 }
2026-10-17T17:11:38Z thinkpad: 3.50 5.57 6.99 | macmini (bots): { 109.57 97.67 96.81 }
2026-10-17T17:12:08Z thinkpad: 2.92 5.22 6.83 | macmini (bots): { 108.43 98.44 97.11 }
2026-10-17T17:12:38Z thinkpad: 3.42 5.12 6.74 | macmini (bots): { 101.61 97.86 96.95 }
2026-10-17T17:13:09Z thinkpad: 5.64 5.51 6.82 | macmini (bots): { 101.66 98.17 97.09 }
2026-10-17T17:13:39Z thinkpad: 7.06 5.89 6.91 | macmini (bots): { 88.43 95.36 96.11 }
2026-10-17T17:14:09Z thinkpad: 7.61 6.13 6.96 | macmini (bots): { 79.03 92.54 95.07 }
2026-10-17T17:14:39Z thinkpad: 9.06 6.68 7.12 | macmini (bots): { 92.55 94.57 95.72 }
2026-10-17T17:15:10Z thinkpad: 9.81 7.06 7.22 | macmini (bots): { 93.71 94.49 95.64 }
2026-10-17T17:15:40Z thinkpad: 9.98 7.41 7.34 | macmini (bots): { 92.26 93.86 95.35 }
2026-10-17T17:16:10Z thinkpad: 7.60 7.08 7.23 | macmini (bots): { 88.61 92.91 94.96 }
2026-10-17T17:16:40Z thinkpad: 6.81 6.93 7.17 | macmini (bots): { 96.96 94.47 95.45 }
2026-10-17T17:17:11Z thinkpad: 6.31 6.81 7.12 | macmini (bots): { 102.92 96.14 96.02 }
2026-10-17T17:17:41Z thinkpad: 4.81 6.38 6.97 | macmini (bots): { 91.20 94.16 95.32 }
2026-10-17T17:18:11Z thinkpad: 7.36 6.81 7.09 | macmini (bots): { 93.60 94.31 95.32 }
2026-10-17T17:18:41Z thinkpad: 9.42 7.38 7.27 | macmini (bots): { 90.86 93.57 95.02 }
2026-10-17T17:19:11Z thinkpad: 9.95 7.72 7.39 | macmini (bots): { 88.94 92.94 94.75 }
2026-10-17T17:19:42Z thinkpad: 9.51 7.85 7.45 | macmini (bots): { 94.97 93.93 95.03 }
2026-10-17T17:20:12Z thinkpad: 8.97 7.90 7.48 | macmini (bots): { 80.46 90.39 93.71 }
2026-10-17T17:20:42Z thinkpad: 7.56 7.65 7.41 | macmini (bots): { 74.38 87.95 92.71 }
2026-10-17T17:21:12Z thinkpad: 6.39 7.36 7.32 | macmini (bots): { 77.48 87.26 92.28 }
2026-10-17T17:21:43Z thinkpad: 5.21 6.98 7.19 | macmini (bots): { 82.05 87.48 92.19 }
2026-10-17T17:22:13Z thinkpad: 3.87 6.49 7.02 | macmini (bots): { 88.15 88.45 92.38 }
2026-10-17T17:22:43Z thinkpad: 3.86 6.22 6.91 | macmini (bots): { 100.23 91.12 93.19 }
2026-10-17T17:23:13Z thinkpad: 8.32 7.05 7.16 | macmini (bots): { 84.55 88.39 92.15 }
2026-10-17T17:23:43Z thinkpad: 13.84 8.37 7.59 | macmini (bots): { 73.74 85.38 90.92 }
2026-10-17T17:24:14Z thinkpad: 21.02 10.71 8.40 | macmini (bots): { 71.36 83.72 90.13 }
2026-10-17T17:24:44Z thinkpad: 20.73 11.82 8.85 | macmini (bots): { 82.27 85.04 90.38 }
2026-10-17T17:25:14Z thinkpad: 16.80 11.77 8.94 | macmini (bots): { 78.63 83.92 89.78 }
2026-10-17T17:25:44Z thinkpad: 14.62 11.68 8.99 | macmini (bots): { 82.33 84.06 89.61 }
2026-10-17T17:26:15Z thinkpad: 14.51 11.94 9.17 | macmini (bots): { 74.13 81.89 88.63 }
2026-10-17T17:26:45Z thinkpad: 12.48 11.74 9.20 | macmini (bots): { 75.65 81.46 88.24 }
2026-10-17T17:27:15Z thinkpad: 15.24 12.56 9.56 | macmini (bots): { 71.35 79.78 87.35 }
2026-10-17T17:27:45Z thinkpad: 15.70 12.87 9.76 | macmini (bots): { 68.21 78.17 86.51 }
2026-10-17T17:28:16Z thinkpad: 12.80 12.47 9.72 | macmini (bots): { 79.74 80.10 86.92 }
2026-10-17T17:28:46Z thinkpad: 9.05 11.58 9.51 | macmini (bots): { 101.01 85.27 88.56 }
2026-10-17T17:29:16Z thinkpad: 9.38 11.41 9.53 | macmini (bots): { 93.76 85.18 88.43 }
2026-10-17T17:29:46Z thinkpad: 11.08 11.62 9.65 | macmini (bots): { 74.91 81.56 87.01 }
2026-10-17T17:30:17Z thinkpad: 10.98 11.53 9.69 | macmini (bots): { 61.10 77.50 85.34 }
2026-10-17T17:30:47Z thinkpad: 9.83 11.21 9.64 | macmini (bots): { 51.19 73.56 83.64 }
2026-10-17T17:31:18Z thinkpad: 9.65 11.03 9.63 | macmini (bots): { 49.64 71.03 82.37 }
2026-10-17T17:31:48Z thinkpad: 7.22 10.33 9.44 | macmini (bots): { 53.62 69.90 81.56 }
2026-10-17T17:32:18Z thinkpad: 5.79 9.68 9.25 | macmini (bots): { 61.07 70.36 81.34 }
2026-10-17T17:32:48Z thinkpad: 3.70 8.80 8.97 | macmini (bots): { 60.39 69.24 80.55 }
2026-10-17T17:33:19Z thinkpad: 2.72 8.08 8.73 | macmini (bots): { 57.06 67.51 79.53 }
2026-10-17T17:33:49Z thinkpad: 1.97 7.37 8.47 | macmini (bots): { 65.72 68.41 79.42 }
2026-10-17T17:34:19Z thinkpad: 1.25 6.68 8.20 | macmini (bots): { 62.60 67.40 78.67 }
2026-10-17T17:34:49Z thinkpad: 0.94 6.09 7.96 | macmini (bots): { 71.83 69.24 78.95 }
2026-10-17T17:35:20Z thinkpad: 0.72 5.54 7.71 | macmini (bots): { 66.16 68.07 78.19 }
2026-10-17T17:35:50Z thinkpad: 0.44 5.01 7.47 | macmini (bots): { 70.96 68.91 78.13 }
2026-10-17T17:36:20Z thinkpad: 1.70 4.79 7.30 | macmini (bots): { 79.74 71.45 78.74 }
2026-10-17T17:36:50Z thinkpad: 3.03 4.80 7.22 | macmini (bots): { 79.71 72.75 78.95 }
2026-10-17T17:37:21Z thinkpad: 3.07 4.67 7.10 | macmini (bots): { 85.13 74.60 79.40 }
2026-10-17T17:37:51Z thinkpad: 2.32 4.33 6.91 | macmini (bots): { 84.85 75.70 79.64 }
2026-10-17T17:38:21Z thinkpad: 1.83 4.03 6.73 | macmini (bots): { 86.47 76.79 79.88 }
2026-10-17T17:38:51Z thinkpad: 1.11 3.64 6.51 | macmini (bots): { 106.96 82.74 81.94 }
2026-10-17T17:39:22Z thinkpad: 1.03 3.37 6.33 | macmini (bots): { 114.52 87.01 83.52 }
2026-10-17T17:39:52Z thinkpad: 1.11 3.16 6.16 | macmini (bots): { 103.99 87.05 83.65 }
2026-10-17T17:40:22Z thinkpad: 1.31 3.02 6.02 | macmini (bots): { 100.27 87.59 83.95 }
2026-10-17T17:40:52Z thinkpad: 1.36 2.87 5.87 | macmini (bots): { 98.17 88.44 84.40 }
2026-10-17T17:41:22Z thinkpad: 2.01 2.88 5.78 | macmini (bots): { 96.27 88.67 84.60 }
2026-10-17T17:41:53Z thinkpad: 1.96 2.77 5.65 | macmini (bots): { 96.31 89.44 85.03 }
2026-10-17T17:42:23Z thinkpad: 3.06 2.97 5.62 | macmini (bots): { 80.49 86.30 84.04 }
2026-10-17T17:42:53Z thinkpad: 2.38 2.81 5.49 | macmini (bots): { 66.40 82.24 82.64 }
2026-10-17T17:43:24Z thinkpad: 2.44 2.81 5.40 | macmini (bots): { 56.70 78.37 81.22 }
2026-10-17T17:43:54Z thinkpad: 1.93 2.65 5.26 | macmini (bots): { 61.56 77.39 80.77 }
2026-10-17T17:44:24Z thinkpad: 1.96 2.59 5.16 | macmini (bots): { 58.25 75.14 79.83 }
2026-10-17T17:44:54Z thinkpad: 2.25 2.60 5.08 | macmini (bots): { 56.32 73.21 78.98 }
2026-10-17T17:45:24Z thinkpad: 1.79 2.46 4.95 | macmini (bots): { 62.87 73.14 78.75 }
2026-10-17T17:45:55Z thinkpad: 2.78 2.64 4.93 | macmini (bots): { 62.58 72.22 78.23 }
2026-10-17T17:46:25Z thinkpad: 4.82 3.14 5.03 | macmini (bots): { 58.89 70.44 77.38 }
2026-10-17T17:46:55Z thinkpad: 3.10 2.90 4.88 | macmini (bots): { 61.68 70.08 77.01 }
2026-10-17T17:47:25Z thinkpad: 2.27 2.71 4.75 | macmini (bots): { 70.43 71.40 77.25 }
2026-10-17T17:47:56Z thinkpad: 1.71 2.53 4.62 | macmini (bots): { 70.51 71.56 77.12 }
2026-10-17T17:48:26Z thinkpad: 1.59 2.42 4.52 | macmini (bots): { 68.98 70.98 76.71 }
2026-10-17T17:48:56Z thinkpad: 2.09 2.47 4.47 | macmini (bots): { 67.75 70.60 76.38 }
2026-10-17T17:49:26Z thinkpad: 2.14 2.44 4.39 | macmini (bots): { 61.53 69.01 75.58 }
2026-10-17T17:49:56Z thinkpad: 2.22 2.43 4.33 | macmini (bots): { 58.49 67.45 74.78 }
2026-10-17T17:50:27Z thinkpad: 1.98 2.36 4.24 | macmini (bots): { 66.15 68.33 74.84 }
2026-10-17T17:50:57Z thinkpad: 2.38 2.42 4.20 | macmini (bots): { 61.65 67.22 74.22 }
2026-10-17T17:51:27Z thinkpad: 3.62 2.72 4.24 | macmini (bots): { 54.05 64.88 73.13 }
2026-10-17T17:51:57Z thinkpad: 3.25 2.70 4.19 | macmini (bots): { 49.14 62.62 72.02 }
2026-10-17T17:52:28Z thinkpad: 2.45 2.55 4.09 | macmini (bots): { 43.72 60.06 70.77 }
2026-10-17T17:52:58Z thinkpad: 2.85 2.62 4.06 | macmini (bots): { 42.53 58.22 69.73 }
2026-10-17T17:53:28Z thinkpad: 8.44 3.97 4.46 | macmini (bots): { 45.21 57.34 69.01 }
2026-10-17T17:53:58Z thinkpad: 10.75 4.91 4.76 | macmini (bots): { 44.86 56.01 68.11 }
2026-10-17T17:54:29Z thinkpad: 8.36 4.90 4.76 | macmini (bots): { 51.73 56.60 67.91 }
2026-10-17T17:54:59Z thinkpad: 7.41 4.98 4.79 | macmini (bots): { 55.80 57.05 67.67 }
2026-10-17T17:55:29Z thinkpad: 5.45 4.73 4.71 | macmini (bots): { 50.40 55.67 66.81 }
2026-10-17T17:55:59Z thinkpad: 6.26 4.97 4.79 | macmini (bots): { 57.22 56.88 66.87 }
2026-10-17T17:56:30Z thinkpad: 8.22 5.52 4.98 | macmini (bots): { 48.94 54.86 65.78 }
2026-10-17T17:57:00Z thinkpad: 6.71 5.42 4.96 | macmini (bots): { 50.54 54.70 65.35 }
2026-10-17T17:57:30Z thinkpad: 4.94 5.14 4.89 | macmini (bots): { 58.02 56.25 65.55 }
2026-10-17T17:58:01Z thinkpad: 4.54 5.04 4.86 | macmini (bots): { 53.98 55.38 64.90 }
2026-10-17T17:58:31Z thinkpad: 4.65 5.04 4.87 | macmini (bots): { 49.96 54.23 64.16 }
2026-10-17T17:59:01Z thinkpad: 5.29 5.16 4.92 | macmini (bots): { 49.63 53.68 63.61 }
2026-10-17T17:59:31Z thinkpad: 4.23 4.94 4.85 | macmini (bots): { 57.15 55.10 63.78 }
2026-10-17T18:00:01Z thinkpad: 3.57 4.72 4.78 | macmini (bots): { 61.70 56.26 63.89 }
2026-10-17T18:00:32Z thinkpad: 3.16 4.52 4.71 | macmini (bots): { 62.10 56.86 63.79 }
2026-10-17T18:01:02Z thinkpad: 2.81 4.29 4.63 | macmini (bots): { 76.29 60.79 64.98 }
2026-10-17T18:01:32Z thinkpad: 7.77 5.26 4.93 | macmini (bots): { 75.36 62.16 65.34 }
2026-10-17T18:02:02Z thinkpad: 11.60 6.45 5.34 | macmini (bots): { 70.83 62.28 65.27 }
2026-10-17T18:02:33Z thinkpad: 11.54 6.97 5.56 | macmini (bots): { 65.66 61.87 65.01 }
2026-10-17T18:03:03Z thinkpad: 10.80 7.33 5.73 | macmini (bots): { 63.09 61.54 64.78 }
2026-10-17T18:03:33Z thinkpad: 10.52 7.56 5.85 | macmini (bots): { 60.31 61.12 64.52 }
2026-10-17T18:04:04Z thinkpad: 10.27 7.76 5.97 | macmini (bots): { 60.99 61.21 64.44 }
2026-10-17T18:04:34Z thinkpad: 8.77 7.69 6.01 | macmini (bots): { 56.72 60.19 63.96 }
2026-10-17T18:05:05Z thinkpad: 5.77 7.06 5.85 | macmini (bots): { 56.14 59.70 63.65 }
2026-10-17T18:05:35Z thinkpad: 5.16 6.79 5.80 | macmini (bots): { 51.25 58.17 62.95 }
2026-10-17T18:06:05Z thinkpad: 4.16 6.36 5.69 | macmini (bots): { 52.03 57.64 62.59 }
2026-10-17T18:06:35Z thinkpad: 4.28 6.18 5.65 | macmini (bots): { 64.66 60.00 63.27 }
2026-10-17T18:07:06Z thinkpad: 7.47 6.76 5.86 | macmini (bots): { 56.20 58.45 62.59 }
2026-10-17T18:07:36Z thinkpad: 6.95 6.74 5.89 | macmini (bots): { 51.92 57.20 62.00 }
2026-10-17T18:08:06Z thinkpad: 7.70 6.99 6.00 | macmini (bots): { 51.14 56.65 61.64 }
2026-10-17T18:08:36Z thinkpad: 5.83 6.60 5.90 | macmini (bots): { 48.08 55.33 60.98 }
2026-10-17T18:09:07Z thinkpad: 4.89 6.30 5.82 | macmini (bots): { 53.80 56.00 61.00 }
2026-10-17T18:09:37Z thinkpad: 4.93 6.19 5.80 | macmini (bots): { 60.49 57.24 61.27 }
2026-10-17T18:10:07Z thinkpad: 4.61 5.98 5.74 | macmini (bots): { 58.95 57.21 61.12 }
2026-10-17T18:10:37Z thinkpad: 4.74 5.91 5.73 | macmini (bots): { 60.84 57.65 61.13 }
2026-10-17T18:11:07Z thinkpad: 3.96 5.61 5.63 | macmini (bots): { 64.23 58.82 61.43 }
2026-10-17T18:11:38Z thinkpad: 4.16 5.53 5.61 | macmini (bots): { 75.59 62.26 62.60 }
2026-10-17T18:12:08Z thinkpad: 2.97 5.11 5.46 | macmini (bots): { 75.25 63.65 63.10 }
@@ -1,695 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T17:31:40+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "7bbbf6e97fb53683eae0a01ce2c134e53cb649a7",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 180,
"seed": 12345,
"explore_speed": 0,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/code/horizon/.claude/worktrees/server-host-4/.local/bench-out/walk-explore.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/server-host-4/target/release/horizon-bots",
"connect": "<server address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 9.62,
"mspt": {
"average": 4.01,
"p50": 3.54,
"p95": 8.19,
"p99": 25.59
},
"mspt_worst_p99": 47.5,
"tps": 20.0,
"rss_mb": {
"mean": 782.52,
"max": 785.3
},
"cpu_percent": 58.87,
"chunks_per_second": 9.6,
"unique_per_second": 0.43,
"forgotten_per_second": 9.65,
"waited_ticks_per_second": 0.0,
"chunks_total": 2697,
"chunks_per_bot": {
"walk": 2697.0
},
"unique_chunks_per_bot": {
"walk": 693.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 14.85,
"mspt": {
"average": 18.22,
"p50": 17.99,
"p95": 29.34,
"p99": 50.06
},
"mspt_worst_p99": 395.5,
"tps": 20.0,
"rss_mb": {
"mean": 3429.06,
"max": 3446.3
},
"cpu_percent": 68.81,
"chunks_per_second": 10.47,
"unique_per_second": 0.47,
"forgotten_per_second": 10.53,
"waited_ticks_per_second": 0.0,
"chunks_total": 2755,
"chunks_per_bot": {
"walk": 2755.0
},
"unique_chunks_per_bot": {
"walk": 697.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 9.21,
"mspt": {
"average": 8.79,
"p50": 8.05,
"p95": 17.35,
"p99": 30.47
},
"mspt_worst_p99": 48.8,
"tps": 20.0,
"rss_mb": {
"mean": 781.88,
"max": 782.8
},
"cpu_percent": 119.16,
"chunks_per_second": 94.63,
"unique_per_second": 3.58,
"forgotten_per_second": 94.31,
"waited_ticks_per_second": 0.0,
"chunks_total": 26003,
"chunks_per_bot": {
"walk": 2600.3
},
"unique_chunks_per_bot": {
"walk": 679.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 15.14,
"mspt": {
"average": 18.67,
"p50": 15.8,
"p95": 38.26,
"p99": 55.93
},
"mspt_worst_p99": 101.8,
"tps": 20.0,
"rss_mb": {
"mean": 3497.35,
"max": 3499.4
},
"cpu_percent": 100.8,
"chunks_per_second": 101.1,
"unique_per_second": 4.38,
"forgotten_per_second": 101.15,
"waited_ticks_per_second": 0.0,
"chunks_total": 26537,
"chunks_per_bot": {
"walk": 2653.7
},
"unique_chunks_per_bot": {
"walk": 687.5
},
"resent": 0,
"pulled_back": 5,
"moved_too_quickly": 5,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 9.82,
"mspt": {
"average": 15.01,
"p50": 13.12,
"p95": 31.12,
"p99": 50.62
},
"mspt_worst_p99": 101.6,
"tps": 20.0,
"rss_mb": {
"mean": 799.52,
"max": 799.7
},
"cpu_percent": 161.02,
"chunks_per_second": 234.3,
"unique_per_second": 8.49,
"forgotten_per_second": 234.35,
"waited_ticks_per_second": 0.0,
"chunks_total": 65926,
"chunks_per_bot": {
"walk": 2637.04
},
"unique_chunks_per_bot": {
"walk": 683.96
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 14.95,
"mspt": {
"average": 18.78,
"p50": 17.47,
"p95": 28.16,
"p99": 51.03
},
"mspt_worst_p99": 107.6,
"tps": 20.0,
"rss_mb": {
"mean": 3520.17,
"max": 3573.8
},
"cpu_percent": 154.9,
"chunks_per_second": 252.39,
"unique_per_second": 11.89,
"forgotten_per_second": 251.75,
"waited_ticks_per_second": 0.0,
"chunks_total": 66469,
"chunks_per_bot": {
"walk": 2658.76
},
"unique_chunks_per_bot": {
"walk": 689.68
},
"resent": 0,
"pulled_back": 10,
"moved_too_quickly": 10,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 9.52,
"mspt": {
"average": 22.72,
"p50": 16.98,
"p95": 60.19,
"p99": 84.16
},
"mspt_worst_p99": 105.4,
"tps": 20.0,
"rss_mb": {
"mean": 813.5,
"max": 815.8
},
"cpu_percent": 179.85,
"chunks_per_second": 464.75,
"unique_per_second": 16.94,
"forgotten_per_second": 464.69,
"waited_ticks_per_second": 0.0,
"chunks_total": 134839,
"chunks_per_bot": {
"walk": 2696.78
},
"unique_chunks_per_bot": {
"walk": 685.48
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 16.36,
"mspt": {
"average": 34.3,
"p50": 31.07,
"p95": 55.55,
"p99": 92.32
},
"mspt_worst_p99": 452.1,
"tps": 20.0,
"rss_mb": {
"mean": 3577.02,
"max": 3611.2
},
"cpu_percent": 267.05,
"chunks_per_second": 496.77,
"unique_per_second": 26.74,
"forgotten_per_second": 496.2,
"waited_ticks_per_second": 0.0,
"chunks_total": 133505,
"chunks_per_bot": {
"walk": 2670.1
},
"unique_chunks_per_bot": {
"walk": 688.8
},
"resent": 0,
"pulled_back": 39,
"moved_too_quickly": 39,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 9.51,
"mspt": {
"average": 63.33,
"p50": 58.89,
"p95": 125.04,
"p99": 175.12
},
"mspt_worst_p99": 261.5,
"tps": 15.79,
"rss_mb": {
"mean": 868.12,
"max": 869.9
},
"cpu_percent": 245.64,
"chunks_per_second": 678.18,
"unique_per_second": 31.8,
"forgotten_per_second": 676.4,
"waited_ticks_per_second": 0.0,
"chunks_total": 219787,
"chunks_per_bot": {
"walk": 2197.87
},
"unique_chunks_per_bot": {
"walk": 637.91
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 34
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 15.14,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 9.72,
"mspt": {
"average": 9.73,
"p50": 8.75,
"p95": 20.99,
"p99": 34.17
},
"mspt_worst_p99": 73.9,
"tps": 20.0,
"rss_mb": {
"mean": 836.18,
"max": 891.0
},
"cpu_percent": 329.64,
"chunks_per_second": 41.11,
"unique_per_second": 35.96,
"forgotten_per_second": 41.08,
"waited_ticks_per_second": 0.16,
"chunks_total": 9460,
"chunks_per_bot": {
"explore": 9460.0
},
"unique_chunks_per_bot": {
"explore": 8479.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.54,
"mspt": {
"average": 9.71,
"p50": 7.86,
"p95": 20.37,
"p99": 76.15
},
"mspt_worst_p99": 388.6,
"tps": 20.0,
"rss_mb": {
"mean": 3614.97,
"max": 3688.5
},
"cpu_percent": 247.15,
"chunks_per_second": 35.47,
"unique_per_second": 33.61,
"forgotten_per_second": 35.45,
"waited_ticks_per_second": 0.0,
"chunks_total": 7912,
"chunks_per_bot": {
"explore": 7912.0
},
"unique_chunks_per_bot": {
"explore": 7580.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 21.27,
"p50": 19.5,
"p95": 39.15,
"p99": 54.23
},
"mspt_worst_p99": 105.5,
"tps": 20.0,
"rss_mb": {
"mean": 1177.66,
"max": 1392.9
},
"cpu_percent": 321.97,
"chunks_per_second": 46.47,
"unique_per_second": 46.11,
"forgotten_per_second": 43.12,
"waited_ticks_per_second": 111.25,
"chunks_total": 14103,
"chunks_per_bot": {
"explore": 1410.3
},
"unique_chunks_per_bot": {
"explore": 1402.4
},
"resent": 0,
"pulled_back": 9856,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.24,
"mspt": {
"average": 16.94,
"p50": 14.24,
"p95": 30.28,
"p99": 109.7
},
"mspt_worst_p99": 1861.2,
"tps": 20.0,
"rss_mb": {
"mean": 3953.29,
"max": 4232.0
},
"cpu_percent": 254.11,
"chunks_per_second": 24.99,
"unique_per_second": 25.01,
"forgotten_per_second": 25.33,
"waited_ticks_per_second": 77.93,
"chunks_total": 7707,
"chunks_per_bot": {
"explore": 770.7
},
"unique_chunks_per_bot": {
"explore": 769.5
},
"resent": 0,
"pulled_back": 11123,
"moved_too_quickly": 20,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 9.92,
"mspt": {
"average": 25.76,
"p50": 23.58,
"p95": 45.01,
"p99": 61.43
},
"mspt_worst_p99": 108.8,
"tps": 20.0,
"rss_mb": {
"mean": 1172.1,
"max": 1482.3
},
"cpu_percent": 314.47,
"chunks_per_second": 61.38,
"unique_per_second": 60.7,
"forgotten_per_second": 55.11,
"waited_ticks_per_second": 304.54,
"chunks_total": 24085,
"chunks_per_bot": {
"explore": 963.4
},
"unique_chunks_per_bot": {
"explore": 954.76
},
"resent": 0,
"pulled_back": 31264,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 15.76,
"mspt": {
"average": 24.93,
"p50": 22.04,
"p95": 37.09,
"p99": 168.46
},
"mspt_worst_p99": 1724.8,
"tps": 20.0,
"rss_mb": {
"mean": 4040.42,
"max": 4447.6
},
"cpu_percent": 305.47,
"chunks_per_second": 37.91,
"unique_per_second": 37.84,
"forgotten_per_second": 38.39,
"waited_ticks_per_second": 258.66,
"chunks_total": 16865,
"chunks_per_bot": {
"explore": 674.6
},
"unique_chunks_per_bot": {
"explore": 671.28
},
"resent": 0,
"pulled_back": 33467,
"moved_too_quickly": 23,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 9.41,
"mspt": {
"average": 39.31,
"p50": 36.98,
"p95": 65.33,
"p99": 88.36
},
"mspt_worst_p99": 157.6,
"tps": 20.0,
"rss_mb": {
"mean": 1250.32,
"max": 1517.1
},
"cpu_percent": 324.77,
"chunks_per_second": 114.99,
"unique_per_second": 112.25,
"forgotten_per_second": 104.41,
"waited_ticks_per_second": 543.77,
"chunks_total": 50402,
"chunks_per_bot": {
"explore": 1008.04
},
"unique_chunks_per_bot": {
"explore": 997.42
},
"resent": 0,
"pulled_back": 64163,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 14.55,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 9.31,
"mspt": {
"average": 52.7,
"p50": 49.49,
"p95": 85.73,
"p99": 115.34
},
"mspt_worst_p99": 219.0,
"tps": 18.97,
"rss_mb": {
"mean": 1358.32,
"max": 3047.2
},
"cpu_percent": 327.95,
"chunks_per_second": 154.2,
"unique_per_second": 151.83,
"forgotten_per_second": 141.65,
"waited_ticks_per_second": 1132.9,
"chunks_total": 84433,
"chunks_per_bot": {
"explore": 844.33
},
"unique_chunks_per_bot": {
"explore": 839.47
},
"resent": 0,
"pulled_back": 116161,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 35
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 16.47,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -1,137 +0,0 @@
# Horizon's server against vanilla 26.3: mixed bots, equal work (2026-10-18)
This reruns `../server-mixed-2026-10-18/` with the bots' load made the same on both servers. In that run the
`explore` bots flew as fast as each server sent them chunks. Horizon kept up and vanilla didn't, so Horizon
sent 2.9 to 5.9 times as many chunks a second, and vanilla's walking bots got a fifth of the chunks Horizon's
got. The comparison of tick times wasn't fair.
## Summary
- **The work was equal** at 1 and 10 bots. At 25 bots it was equal in chunks sent a second (158.6 against 157.6);
vanilla was still catching up on chunks its bots hadn't had yet, see "Equal work" below. Every bot joined, none
was dropped, no chunk was sent twice, and no bot ever waited for the chunk it stood in.
- **Tick time (MSPT), same work:**
- 1 bot: 1.8 ms on Horizon against 11.3 on vanilla;
- 10 bots: 4.7 against 34.3 ms;
- 25 bots: 11.7 against 50.5 ms. Vanilla dropped just under 20 TPS (19.8) and its worst 5-second P99 was
1388 ms, against Horizon's 130;
- 50 bots: Horizon ran at 23.8 ms (20 TPS). Vanilla's watchdog stopped its server 30 s after the last bot
joined ("A single server tick took 60.01 seconds").
- **Memory:** Horizon's RSS was 4.1 to 4.9 times smaller: 0.69 against 3.40 GB with one bot, 0.95 against
3.87 GB with 25, and 1.03 GB with 50. Vanilla's heap is fixed at 4 GB, so its RSS is mostly that heap.
- **CPU** (100 % is one of the machine's 4 threads): 30 against 128 % at 10 bots, 58 against 192 % at 25.
- **What changed since the first run:** the first run's vanilla held 50 bots at 29.7 ms because its walking
bots got 640 to 1100 chunks each. With the explore bots paced, vanilla serves the walking bots in full
(5400 to 6100 chunks each, against Horizon's 6100 to 6600), and that is the load it can't carry at 50.
## Results
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and P99
are over the 300 measured seconds. "Worst P99" is the highest P99 of a single 5-second query. RSS and CPU
cover the server's process tree. "First-time chunks/s" counts chunks a bot hadn't received before (summed
over the bots). "Waited" is ticks a second that bots stood still waiting for their own chunk. "Pulled back"
counts position packets that moved a bot back.
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | First-time chunks/s | Waited | Pulled back |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | **1.77** | 4.21 | 13.04 | 47.5 | 20 | **0.69 / 0.69 GB** | 28 % | 19.7 s | 9.94 | 0.01 | 0 | 0 |
| 1 | vanilla | 11.34 | 17.12 | 33.22 | 119.1 | 20 | 3.40 / 3.42 GB | 36 % | 20.4 s | 9.98 | 0.01 | 0 | 0 |
| 10 | Horizon | **4.74** | 10.48 | 19.69 | 120.1 | 20 | **0.86 / 0.88 GB** | 30 % | 19.5 s | 61.35 | 1.79 | 0 | 4159 |
| 10 | vanilla | 34.30 | 53.72 | 89.08 | 375.8 | 20 | 3.71 / 3.75 GB | 128 % | 19.9 s | 61.66 | 1.93 | 0 | 3976 |
| 25 | Horizon | **11.72** | 24.45 | 43.79 | 129.8 | 20 | **0.95 / 1.09 GB** | 58 % | 9.6 s | 158.57 | 5.78 | 0 | 3496 |
| 25 | vanilla | 50.45 | 74.09 | 191.21 | 1388.1 | 19.82 | 3.87 / 4.75 GB | 192 % | 37.8 s | 157.63 | 8.25 | 0 | 3629 |
| 50 | Horizon | **23.82** | 50.98 | 74.64 | 195.9 | 20 | **1.03 / 1.20 GB** | 98 % | 14.9 s | 302.81 | 10.96 | 0 | 7565 |
| 50 | vanilla | stopped by its watchdog | | | | | | | 11.2 s | | | | |
Chunks each bot received over the whole run, by behaviour (mean):
| Bots | Server | walk | explore | idle |
|---:|---|---:|---:|---:|
| 1 | Horizon | 6092 | | |
| 1 | vanilla | 6084 | | |
| 10 | Horizon | 6248 | 1165 | 473 |
| 10 | vanilla | 5729 | 1124 | 473 |
| 25 | Horizon | 6629 | 1267 | 473 |
| 25 | vanilla | 5387 | 1119 | 473 |
| 50 | Horizon | 6497 | 1256 | 473 |
Both servers pulled bots back about as often (`moved too quickly` 20 and 296 times on vanilla at 10 and 25
bots).
## Equal work
`tools/server_bench.py` now prints a work table after the runs. At each bot count it compares every server's
chunks sent a second and first-time chunks a second with the busiest server's. Work counts as equal when
both are within 10 % (or within a chunk a second when there are few). The report keeps the table as `work`.
| Bots | Chunks/s (Horizon, vanilla) | First-time chunks/s (Horizon, vanilla) | Equal |
|---:|---|---|---|
| 1 | 9.94, 9.98 | 0.01, 0.01 | yes |
| 10 | 61.35, 61.66 | 1.79, 1.93 | yes |
| 25 | 158.57, 157.63 | 5.78, 8.25 | no: vanilla sent 2.5 more first-time chunks a second |
Two settings made the load the same:
- **Explore bots paced at 0.8 blocks a second** (`--explore-speed 0.8`), the rate the 2026-10-17 run showed
both servers keep up with. Every bot then follows the same path on both servers. The path is deterministic
per bot, and a bot only stops while the chunk it stands in hasn't arrived, which never happened here
(`Waited` 0). So both servers had the same chunks to send.
- **A 300-second warm-up** before the 300 measured seconds. A first pass with the old 60-second warm-up
(aborted after 10 bots) sent the same chunks a second on both (63.1 against 61.4). But vanilla was still
delivering the bots' first views during the measurement (12.9 first-time chunks a second against Horizon's
4.1), so its window held more work than Horizon's. With 300 seconds both were in steady state at 1 and 10
bots. At 25, vanilla still sent 2.5 more first-time chunks a second than Horizon: 1.6 % of all it sent, and
its walking bots ended the run with 19 % fewer chunks. Read the 25-bot tick times with that in mind: the
extra first-time chunks are work vanilla had still to do because it fell behind.
## Method
`tools/server_bench.py` ran each configuration on a fresh server:
- the same `server.properties` on both: seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`, white-list off;
- `gamerule respawn_radius 0`, and otherwise vanilla's default game rules (the locator bar on for both);
- the server waits for `Done`, the bots join one a second, warm up for 300 seconds and are measured for 300,
with `/tick query` every 5 seconds and RSS and CPU every second.
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice deterministic
per bot. All are creative and fly 24 blocks above spawn. `walk` flies to random points within 48 blocks at
5.6 blocks a second, `explore` flies straight out at 0.8 blocks a second, and `idle` hovers.
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS. The desktop (24 threads)
was meant to run it, but it didn't wake (`~/fleet/power wake desktop` timed out, as it did for the first run).
No other session ran on the ThinkPad during the runs (`load.txt`, a sample a minute). The servers ran under
`nice -n 19`. The bots ran on the Mac mini (M4 Pro, shared with other work at 70 to 90 % CPU) and reached the
ThinkPad over the tailnet.
**What ran.**
- Horizon: the server build from #916 (`horizon-client --no-default-features`, no sound or controllers) at
519ec121, packaged with `xtask package-server` and installed with `install.sh --archive`, as Linux owners
will get it. `horizon-server` ran it as `horizon-engine`. The game files were prepared from the owner's own
Minecraft. `horizon-bots` and `server_bench.py` came from this PR.
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
`server_bench.py` stopped after vanilla's 50-bot server crashed and before it wrote its report.
`mixed-equal.json` holds the run lines it printed. The vanilla 50 entry and its error come from that server's
log, and the `work` table was computed with `server_bench.work`. `load.txt` is the fleet monitor's view of the
machines during the runs.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
HORIZON_BENCH_BIN=~/.local/share/horizon/current \
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50 --behaviour mixed \
--explore-speed 0.8 --warmup 300 --nice 19 \
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
--out mixed-equal.json
## Next
- One pass per configuration ran. Vanilla at 50 bots, in particular, needs a second pass, ideally on the
desktop (24 threads), where neither server is CPU-bound.
- 100 bots, and Paper and Fabric + Lithium (docs/server.md section 9).
- A longer warm-up, or measuring from when the first-time chunk rate settles, so the 25-bot work is equal too.
@@ -1,702 +0,0 @@
2026-10-18T19:32:39Z
ludothinkpad ok: 27.2 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini ok: 25.9 GB free, 14 threads 73% busy, 1 sessions
macbook offline
RAM 27.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 42% busy (agents 0.0, rest 1.6 cores) · 80°C · load 5.9
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:33:44Z
ludothinkpad ok: 26.7 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 93% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 34% busy (agents 0.0, rest 1.7 cores) · 81°C · load 4.0
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:34:48Z
ludothinkpad ok: 26.4 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 90% busy, 1 sessions
macbook offline
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 54% busy (agents 0.0, rest 2.2 cores) · 79°C · load 2.8
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:35:52Z
ludothinkpad ok: 26.7 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 25.4 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 30% busy (agents 0.0, rest 1.1 cores) · 82°C · load 2.2
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:36:57Z
ludothinkpad ok: 26.7 GB free, 4 threads 25% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 93% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 37% busy (agents 0.0, rest 1.2 cores) · 80°C · load 1.5
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:38:01Z
ludothinkpad ok: 26.6 GB free, 4 threads 54% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 64% busy, 1 sessions
macbook offline
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 21% busy (agents 0.0, rest 1.2 cores) · 81°C · load 1.9
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:39:05Z
ludothinkpad ok: 26.7 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 70% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 28% busy (agents 0.0, rest 1.0 cores) · 81°C · load 1.3
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:40:10Z
ludothinkpad ok: 26.7 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 73% busy, 1 sessions
macbook offline
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 32% busy (agents 0.0, rest 1.7 cores) · 80°C · load 0.9
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:41:14Z
ludothinkpad ok: 26.7 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 28.8 GB free, 14 threads 77% busy, 1 sessions
macbook offline
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 15% busy (agents 0.0, rest 1.1 cores) · 82°C · load 1.3
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:42:19Z
ludothinkpad ok: 26.5 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini busy: 26.1 GB free, 14 threads 94% busy, 1 sessions
macbook offline
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 30% busy (agents 0.0, rest 1.3 cores) · 78°C · load 1.1
· disk 289.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:43:23Z
ludothinkpad busy: 26.7 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.2 GB free, 14 threads 83% busy, 1 sessions
macbook offline
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 3.8 cores) · 86°C · load 2.4
· disk 289.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:44:27Z
ludothinkpad busy: 25.2 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini ok: 29.0 GB free, 14 threads 73% busy, 1 sessions
macbook offline
RAM 24.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 83% busy (agents 0.0, rest 3.7 cores) · 83°C · load 5.5
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:45:31Z
ludothinkpad ok: 24.5 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini busy: 27.0 GB free, 14 threads 93% busy, 1 sessions
macbook offline
RAM 24.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 34% busy (agents 0.0, rest 1.5 cores) · 84°C · load 3.5
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:46:35Z
ludothinkpad ok: 23.7 GB free, 4 threads 69% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 90% busy, 1 sessions
macbook offline
RAM 23.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 75% busy (agents 0.0, rest 2.8 cores) · 87°C · load 2.7
· disk 289.5 GB free · battery 96% · Ludovic: away
2026-10-18T19:47:40Z
ludothinkpad ok: 23.8 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini busy: 25.9 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 23.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 59% busy (agents 0.0, rest 2.5 cores) · 83°C · load 2.9
· disk 291.7 GB free · battery 96% · Ludovic: away
2026-10-18T19:48:44Z
ludothinkpad ok: 24.2 GB free, 4 threads 61% busy, 0 sessions
desktop offline
macmini busy: 29.0 GB free, 14 threads 90% busy, 1 sessions
macbook offline
RAM 24.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 62% busy (agents 0.0, rest 2.5 cores) · 84°C · load 3.2
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:49:48Z
ludothinkpad ok: 25.1 GB free, 4 threads 59% busy, 0 sessions
desktop offline
macmini ok: 31.1 GB free, 14 threads 81% busy, 1 sessions
macbook offline
RAM 25.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 57% busy (agents 0.0, rest 2.4 cores) · 85°C · load 2.9
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:50:52Z
ludothinkpad ok: 25.7 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini busy: 28.4 GB free, 14 threads 88% busy, 2 sessions
macbook offline
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 18% busy (agents 0.0, rest 1.4 cores) · 67°C · load 2.4
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:51:57Z
ludothinkpad ok: 23.7 GB free, 4 threads 40% busy, 0 sessions
desktop offline
macmini ok: 27.9 GB free, 14 threads 83% busy, 1 sessions
macbook offline
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 23% busy (agents 0.0, rest 1.4 cores) · 81°C · load 1.8
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:53:01Z
ludothinkpad ok: 25.3 GB free, 4 threads 25% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 84% busy, 1 sessions
macbook offline
RAM 25.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 31% busy (agents 0.0, rest 1.2 cores) · 80°C · load 1.8
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:54:05Z
ludothinkpad ok: 26.5 GB free, 4 threads 75% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
RAM 26.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 69% busy (agents 0.0, rest 2.5 cores) · 78°C · load 2.3
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:55:09Z
ludothinkpad busy: 26.4 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 86% busy, 1 sessions
macbook offline
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 32% busy (agents 0.0, rest 2.4 cores) · 79°C · load 2.9
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:56:14Z
ludothinkpad ok: 26.4 GB free, 4 threads 39% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 68% busy, 1 sessions
macbook offline
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 63% busy (agents 0.0, rest 1.8 cores) · 82°C · load 2.3
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:57:18Z
ludothinkpad ok: 26.3 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 29.2 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 19% busy (agents 0.0, rest 1.0 cores) · 80°C · load 1.8
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:58:22Z
ludothinkpad ok: 26.3 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini busy: 27.3 GB free, 14 threads 88% busy, 1 sessions
macbook offline
RAM 26.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 93% busy (agents 0.0, rest 2.5 cores) · 80°C · load 2.0
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T19:59:27Z
ludothinkpad ok: 26.2 GB free, 4 threads 52% busy, 0 sessions
desktop offline
macmini ok: 35.0 GB free, 14 threads 85% busy, 1 sessions
macbook offline
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 22% busy (agents 0.0, rest 1.8 cores) · 80°C · load 3.6
· disk 291.4 GB free · battery 96% · Ludovic: away
2026-10-18T20:00:31Z
ludothinkpad busy: 25.8 GB free, 4 threads 87% busy, 0 sessions
desktop offline
macmini busy: 33.0 GB free, 14 threads 87% busy, 1 sessions
macbook offline
RAM 27.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 51% busy (agents 0.0, rest 2.9 cores) · 79°C · load 3.6
· disk 291.5 GB free · battery 96% · Ludovic: away
2026-10-18T20:01:35Z
ludothinkpad ok: 25.6 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 32.4 GB free, 14 threads 81% busy, 1 sessions
macbook ok: 7.2 GB free, 12 threads 32% busy, 0 sessions
RAM 25.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 34% busy (agents 0.0, rest 1.3 cores) · 81°C · load 2.5
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T20:02:40Z
ludothinkpad ok: 26.6 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 79% busy, 1 sessions
macbook ok: 6.5 GB free, 12 threads 39% busy, 0 sessions
RAM 26.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 33% busy (agents 0.0, rest 1.3 cores) · 81°C · load 2.7
· disk 291.6 GB free · battery 96% · Ludovic: away
2026-10-18T20:03:44Z
ludothinkpad ok: 25.5 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 31.1 GB free, 14 threads 89% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 6% busy, 0 sessions
RAM 25.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 66% busy (agents 0.0, rest 2.5 cores) · 87°C · load 2.5
· disk 291.3 GB free · battery 96% · Ludovic: away
2026-10-18T20:04:48Z
ludothinkpad busy: 26.2 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini busy: 30.0 GB free, 14 threads 89% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 31% busy, 0 sessions
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 84% busy (agents 0.0, rest 3.7 cores) · 85°C · load 3.4
· disk 291.3 GB free · battery 96% · Ludovic: away
2026-10-18T20:05:52Z
ludothinkpad busy: 24.5 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 34.1 GB free, 14 threads 89% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
RAM 24.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 100% busy (agents 0.0, rest 4.0 cores) · 83°C · load 9.0
· disk 291.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:06:57Z
ludothinkpad busy: 23.5 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 88% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 5% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 10.1
· disk 309.2 GB free · battery 96% · Ludovic: away
2026-10-18T20:08:01Z
ludothinkpad busy: 23.4 GB free, 4 threads 88% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 83% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 2% busy, 0 sessions
RAM 23.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 77% busy (agents 0.0, rest 3.4 cores) · 84°C · load 7.5
· disk 309.2 GB free · battery 96% · Ludovic: away
2026-10-18T20:09:05Z
ludothinkpad busy: 24.0 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini ok: 25.9 GB free, 14 threads 83% busy, 1 sessions
macbook ok: 7.1 GB free, 12 threads 19% busy, 0 sessions
RAM 23.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 91% busy (agents 0.0, rest 3.6 cores) · 82°C · load 5.9
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:10:09Z
ludothinkpad busy: 24.0 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 88% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 19% busy, 0 sessions
RAM 24.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 92% busy (agents 0.0, rest 3.7 cores) · 84°C · load 6.6
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:11:14Z
ludothinkpad busy: 24.3 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 18% busy, 0 sessions
RAM 24.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 3.7 cores) · 85°C · load 6.5
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:12:18Z
ludothinkpad busy: 24.7 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini ok: 28.6 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 7.3 GB free, 12 threads 20% busy, 0 sessions
RAM 24.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 82% busy (agents 0.0, rest 3.4 cores) · 84°C · load 6.1
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:13:22Z
ludothinkpad ok: 23.4 GB free, 4 threads 53% busy, 0 sessions
desktop offline
macmini ok: 28.1 GB free, 14 threads 77% busy, 1 sessions
macbook ok: 7.3 GB free, 12 threads 20% busy, 0 sessions
RAM 23.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 48% busy (agents 0.0, rest 2.4 cores) · 80°C · load 4.2
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:14:27Z
ludothinkpad ok: 24.0 GB free, 4 threads 44% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 71% busy, 1 sessions
macbook ok: 6.9 GB free, 12 threads 2% busy, 0 sessions
RAM 24.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 23% busy (agents 0.0, rest 2.0 cores) · 77°C · load 4.3
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:15:31Z
ludothinkpad ok: 22.7 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini busy: 27.2 GB free, 14 threads 86% busy, 1 sessions
macbook ok: 6.9 GB free, 12 threads 19% busy, 0 sessions
RAM 22.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 30% busy (agents 0.0, rest 1.3 cores) · 77°C · load 2.6
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:16:35Z
ludothinkpad ok: 26.8 GB free, 4 threads 37% busy, 0 sessions
desktop offline
macmini ok: 28.6 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 9% busy, 0 sessions
RAM 26.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 42% busy (agents 0.0, rest 1.7 cores) · 81°C · load 2.3
· disk 309.1 GB free · battery 96% · Ludovic: away
2026-10-18T20:17:39Z
ludothinkpad ok: 26.3 GB free, 4 threads 63% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 19% busy, 0 sessions
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 64% busy (agents 0.0, rest 2.6 cores) · 87°C · load 1.9
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:18:43Z
ludothinkpad ok: 26.9 GB free, 4 threads 37% busy, 0 sessions
desktop offline
macmini busy: 32.2 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 10% busy, 0 sessions
RAM 26.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 56% busy (agents 0.0, rest 1.9 cores) · 84°C · load 3.9
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:19:48Z
ludothinkpad ok: 26.0 GB free, 4 threads 77% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 83% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 10% busy, 0 sessions
RAM 26.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 66% busy (agents 0.0, rest 2.9 cores) · 86°C · load 3.2
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:20:52Z
ludothinkpad ok: 25.9 GB free, 4 threads 80% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 7.0 GB free, 12 threads 17% busy, 0 sessions
RAM 25.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 68% busy (agents 0.0, rest 3.2 cores) · 86°C · load 4.4
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:21:56Z
ludothinkpad ok: 26.3 GB free, 4 threads 63% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 77% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 14% busy, 0 sessions
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 66% busy (agents 0.0, rest 2.8 cores) · 84°C · load 3.1
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:23:00Z
ludothinkpad ok: 26.8 GB free, 4 threads 74% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 11% busy, 0 sessions
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 65% busy (agents 0.0, rest 2.8 cores) · 84°C · load 5.0
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:24:05Z
ludothinkpad ok: 26.9 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 90% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 8% busy, 0 sessions
RAM 26.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 64% busy (agents 0.0, rest 2.7 cores) · 86°C · load 4.9
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:25:09Z
ludothinkpad ok: 26.0 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 12% busy, 0 sessions
RAM 26.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 49% busy (agents 0.0, rest 2.1 cores) · 72°C · load 5.5
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:26:13Z
ludothinkpad ok: 25.8 GB free, 4 threads 47% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 56% busy (agents 0.0, rest 2.1 cores) · 78°C · load 3.1
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:27:18Z
ludothinkpad busy: 25.5 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 25.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 100% busy (agents 0.0, rest 3.9 cores) · 78°C · load 4.7
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:28:22Z
ludothinkpad busy: 24.6 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 24.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 77°C · load 10.5
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:29:27Z
ludothinkpad busy: 24.3 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 29.8 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 24.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 10.0
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:30:31Z
ludothinkpad busy: 23.7 GB free, 4 threads 91% busy, 0 sessions
desktop offline
macmini ok: 30.1 GB free, 14 threads 71% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 94% busy (agents 0.0, rest 3.8 cores) · 80°C · load 10.3
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:31:36Z
ludothinkpad busy: 23.6 GB free, 4 threads 93% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 81% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 14% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 95% busy (agents 0.0, rest 3.7 cores) · 80°C · load 10.7
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:32:40Z
ludothinkpad busy: 23.6 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 76% busy, 1 sessions
macbook ok: 6.8 GB free, 12 threads 22% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 84% busy (agents 0.0, rest 3.5 cores) · 80°C · load 9.7
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:33:45Z
ludothinkpad ok: 25.2 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 25.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 48% busy (agents 0.0, rest 2.1 cores) · 87°C · load 6.5
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:34:49Z
ludothinkpad ok: 25.1 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 28.2 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 24.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 78% busy (agents 0.0, rest 2.7 cores) · 88°C · load 4.0
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:35:53Z
ludothinkpad busy: 23.6 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini busy: 27.9 GB free, 14 threads 86% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 17% busy, 0 sessions
RAM 23.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 88% busy (agents 0.0, rest 3.3 cores) · 85°C · load 4.9
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:36:58Z
ludothinkpad ok: 23.6 GB free, 4 threads 75% busy, 0 sessions
desktop offline
macmini ok: 29.4 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.5 GB free, 12 threads 18% busy, 0 sessions
RAM 23.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 85% busy (agents 0.0, rest 3.2 cores) · 85°C · load 5.1
· disk 309.0 GB free · battery 96% · Ludovic: away
2026-10-18T20:38:02Z
ludothinkpad ok: 22.6 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini ok: 26.5 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 22.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 54% busy (agents 0.0, rest 2.2 cores) · 85°C · load 4.5
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:39:06Z
ludothinkpad ok: 27.2 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 29.5 GB free, 14 threads 85% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 27.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 48% busy (agents 0.0, rest 2.2 cores) · 87°C · load 4.0
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:40:10Z
ludothinkpad ok: 26.2 GB free, 4 threads 72% busy, 0 sessions
desktop offline
macmini ok: 27.7 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 19% busy, 0 sessions
RAM 26.3 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 80% busy (agents 0.0, rest 3.1 cores) · 86°C · load 10.8
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:41:15Z
ludothinkpad busy: 26.4 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 28.5 GB free, 14 threads 79% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
RAM 26.4 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 90% busy (agents 0.0, rest 3.4 cores) · 86°C · load 12.8
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:42:19Z
ludothinkpad ok: 26.2 GB free, 4 threads 82% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 78% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 11% busy, 0 sessions
RAM 26.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 74% busy (agents 0.0, rest 3.3 cores) · 85°C · load 8.8
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:43:23Z
ludothinkpad busy: 26.7 GB free, 4 threads 93% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
macbook ok: 6.6 GB free, 12 threads 10% busy, 0 sessions
RAM 26.7 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 88% busy (agents 0.0, rest 3.4 cores) · 85°C · load 5.4
· disk 308.9 GB free · battery 96% · Ludovic: away
2026-10-18T20:44:27Z
ludothinkpad ok: 27.0 GB free, 4 threads 82% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 13% busy, 0 sessions
RAM 27.0 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 75% busy (agents 0.0, rest 3.3 cores) · 82°C · load 9.8
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:45:32Z
ludothinkpad ok: 27.2 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 84% busy (agents 0.0, rest 3.2 cores) · 85°C · load 6.9
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:46:36Z
ludothinkpad busy: 27.1 GB free, 4 threads 87% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 92% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 26.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 85% busy (agents 0.0, rest 3.2 cores) · 82°C · load 4.4
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:47:40Z
ludothinkpad busy: 25.3 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 26.5 GB free, 14 threads 72% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 25.5 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 85% busy (agents 0.0, rest 3.4 cores) · 86°C · load 3.7
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:48:45Z
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 28.5 GB free, 14 threads 84% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 15% busy, 0 sessions
RAM 26.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 98% busy (agents 0.0, rest 3.9 cores) · 80°C · load 6.5
· disk 308.8 GB free · battery 96% · Ludovic: away
2026-10-18T20:49:50Z
ludothinkpad ok: 26.8 GB free, 4 threads 71% busy, 0 sessions
desktop offline
macmini busy: 27.7 GB free, 14 threads 93% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 26.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 92% busy (agents 0.0, rest 3.4 cores) · 81°C · load 5.2
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:50:54Z
ludothinkpad busy: 25.8 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.3 GB free, 14 threads 95% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 11% busy, 0 sessions
RAM 25.8 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 99% busy (agents 0.0, rest 4.0 cores) · 82°C · load 8.1
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:51:59Z
ludothinkpad ok: 27.2 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 31.2 GB free, 14 threads 76% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 8% busy, 0 sessions
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 15% busy (agents 0.0, rest 1.3 cores) · 81°C · load 7.9
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:53:03Z
ludothinkpad ok: 27.3 GB free, 4 threads 42% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 27.2 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 14% busy (agents 0.0, rest 0.8 cores) · 82°C · load 3.1
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:54:08Z
ludothinkpad ok: 28.9 GB free, 4 threads 4% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 80% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 9% busy, 0 sessions
RAM 28.9 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 2% busy (agents 0.0, rest 0.3 cores) · 82°C · load 1.3
· disk 308.7 GB free · battery 96% · Ludovic: away
2026-10-18T20:55:12Z
ludothinkpad ok: 27.7 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 31.2 GB free, 14 threads 70% busy, 1 sessions
macbook ok: 6.7 GB free, 12 threads 10% busy, 0 sessions
RAM 27.6 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 26% busy (agents 0.0, rest 1.2 cores) · 69°C · load 1.0
· disk 308.8 GB free · battery 96% · Ludovic: away
@@ -1,421 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-18T19:32:00+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4,
"name": "ThinkPad, Intel Core i7-7500U, 32 GB, NixOS"
},
"commit": "f1d5bf520986b00a5b968a84d85809fca1a039fa",
"engine": "horizon-engine: the server build of #916 (horizon-client --no-default-features) at 519ec121, as install.sh --archive installed it",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 300,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"nice": 19,
"bots_via": "ssh macmini",
"connect": "the ThinkPad's tailnet address",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"note": "server_bench.py was stopped after vanilla's 50-bot server crashed, before it wrote this file; the runs are the lines it printed, and the vanilla 50 entry is taken from that server's log.",
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 19.73,
"mspt": {
"average": 1.77,
"p50": 1.34,
"p95": 4.21,
"p99": 13.04
},
"mspt_worst_p99": 47.5,
"tps": 20.0,
"rss_mb": {
"mean": 686.54,
"max": 686.7
},
"cpu_percent": 27.92,
"chunks_per_second": 9.94,
"unique_per_second": 0.01,
"forgotten_per_second": 9.9,
"waited_ticks_per_second": 0.0,
"chunks_total": 6092,
"chunks_per_bot": {
"walk": 6092.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 20.45,
"mspt": {
"average": 11.34,
"p50": 10.6,
"p95": 17.12,
"p99": 33.22
},
"mspt_worst_p99": 119.1,
"tps": 20.0,
"rss_mb": {
"mean": 3404.16,
"max": 3416.3
},
"cpu_percent": 35.93,
"chunks_per_second": 9.98,
"unique_per_second": 0.01,
"forgotten_per_second": 9.93,
"waited_ticks_per_second": 0.0,
"chunks_total": 6084,
"chunks_per_bot": {
"walk": 6084.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 19.54,
"mspt": {
"average": 4.74,
"p50": 3.83,
"p95": 10.48,
"p99": 19.69
},
"mspt_worst_p99": 120.1,
"tps": 20.0,
"rss_mb": {
"mean": 855.26,
"max": 882.7
},
"cpu_percent": 29.78,
"chunks_per_second": 61.35,
"unique_per_second": 1.79,
"forgotten_per_second": 61.47,
"waited_ticks_per_second": 0.0,
"chunks_total": 40762,
"chunks_per_bot": {
"walk": 6247.67,
"explore": 1165.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.83,
"explore": 1086.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 4159,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 19.95,
"mspt": {
"average": 34.3,
"p50": 32.2,
"p95": 53.72,
"p99": 89.08
},
"mspt_worst_p99": 375.8,
"tps": 20.0,
"rss_mb": {
"mean": 3713.5,
"max": 3754.9
},
"cpu_percent": 128.07,
"chunks_per_second": 61.66,
"unique_per_second": 1.93,
"forgotten_per_second": 61.71,
"waited_ticks_per_second": 0.0,
"chunks_total": 37568,
"chunks_per_bot": {
"walk": 5729.0,
"explore": 1124.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.17,
"explore": 1055.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3976,
"moved_too_quickly": 20,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 9.61,
"mspt": {
"average": 11.72,
"p50": 9.88,
"p95": 24.45,
"p99": 43.79
},
"mspt_worst_p99": 129.8,
"tps": 20.0,
"rss_mb": {
"mean": 947.35,
"max": 1086.1
},
"cpu_percent": 57.83,
"chunks_per_second": 158.57,
"unique_per_second": 5.78,
"forgotten_per_second": 158.45,
"waited_ticks_per_second": 0.0,
"chunks_total": 108126,
"chunks_per_bot": {
"walk": 6628.53,
"explore": 1266.6,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 704.67,
"explore": 1189.2,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3496,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 37.78,
"mspt": {
"average": 50.45,
"p50": 46.39,
"p95": 74.09,
"p99": 191.21
},
"mspt_worst_p99": 1388.1,
"tps": 19.82,
"rss_mb": {
"mean": 3866.18,
"max": 4745.0
},
"cpu_percent": 192.42,
"chunks_per_second": 157.63,
"unique_per_second": 8.25,
"forgotten_per_second": 156.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 88763,
"chunks_per_bot": {
"walk": 5387.0,
"explore": 1118.6,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 699.0,
"explore": 1075.0,
"idle": 473.0
},
"resent": 2,
"pulled_back": 3629,
"moved_too_quickly": 296,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 14.93,
"mspt": {
"average": 23.82,
"p50": 20.11,
"p95": 50.98,
"p99": 74.64
},
"mspt_worst_p99": 195.9,
"tps": 20.0,
"rss_mb": {
"mean": 1031.41,
"max": 1201.2
},
"cpu_percent": 98.21,
"chunks_per_second": 302.81,
"unique_per_second": 10.96,
"forgotten_per_second": 303.47,
"waited_ticks_per_second": 0.0,
"chunks_total": 212181,
"chunks_per_bot": {
"walk": 6496.53,
"explore": 1255.5,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.9,
"explore": 1184.7,
"idle": 473.0
},
"resent": 0,
"pulled_back": 7565,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 11.16,
"error": "stopped by its watchdog 30 s after the last bot joined: \"A single server tick took 60.01 seconds (should be max 0.05)\" (Player.tick)"
}
],
"work": [
{
"behaviour": "mixed",
"bots": 1,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 9.94,
"share": 0.996
},
"unique_per_second": {
"value": 0.01,
"share": 1.0
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 9.98,
"share": 1.0
},
"unique_per_second": {
"value": 0.01,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
},
{
"behaviour": "mixed",
"bots": 10,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 61.35,
"share": 0.995
},
"unique_per_second": {
"value": 1.79,
"share": 0.927
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 61.66,
"share": 1.0
},
"unique_per_second": {
"value": 1.93,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
},
{
"behaviour": "mixed",
"bots": 25,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 158.57,
"share": 1.0
},
"unique_per_second": {
"value": 5.78,
"share": 0.701
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 157.63,
"share": 0.994
},
"unique_per_second": {
"value": 8.25,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": false
}
]
}
@@ -1,168 +0,0 @@
# Horizon's server against vanilla 26.3, equal work (2026-10-17)
The same bots on Horizon's dedicated server and on vanilla 26.3's, one server at a time, on the same
machine, with the work each server does made the same before the numbers are compared. This replaces the
first run in docs/server.md section 9, where the work wasn't equal.
## What was made equal
The first run (PR #556) had three differences that made the comparison unfair. Each is fixed in the code
that ran here:
- **Refused moves.** Vanilla pulls a player back when a move is too fast ("moved too quickly") or ends
inside a block ("moved wrongly"). Horizon now runs both checks the way vanilla does (PR #586), so a bot
that flies too fast is pulled back on both servers.
- **Chunk sending.** Vanilla paces chunks by the client's acknowledgements (PlayerChunkSender: one
unacknowledged batch at first, then ten, each at most the client's desired chunks per tick). Horizon used
to send twice as many per batch; it now paces them the same way (PR #586), and the bots acknowledge
batches and ask for a rate the way vanilla's client does, so both servers send at the pace the client
asks for. Chunks sent again while the bot still held
them (`resent`) are counted: 0 on both servers in every run.
- **Movement gated on chunks.** A bot doesn't move while the chunk it stands in hasn't arrived (vanilla's
client waits the same way), so a slower server isn't asked to teleport bots into terrain it hasn't sent.
One difference isn't in Horizon yet: vanilla 26.3's locator bar (waypoints), which costs vanilla O(players²)
work per move. Every vanilla run here sets `gamerule locator_bar false`, so neither server does it.
## Method
`tools/server_bench.py` runs each configuration on a fresh server:
- the same `server.properties` on both (seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`), and `gamerule respawn_radius 0`;
- the server waits for `Done`, the bots join one a second, a 60-second warm-up, then 300 seconds of
measurement;
- MSPT (average, P50, P95, P99) comes from each server's own `/tick query`, asked every 5 seconds on the
console, so both measure themselves the same way (vanilla's ServerTickRateManager numbers, which Horizon
reproduces); TPS is 20 while the average fits in 50 ms, else 1000 / MSPT;
- RSS and CPU are the server's whole process tree, once a second (100 % is one core);
- chunk counts come from the bots: chunks received per second, chunks no bot had before (new terrain), and
pull-backs (position packets that move a bot back).
The bots are `horizon-bots` (creative, flying 24 blocks above spawn, 20 moves a second):
- `walk`: flies to random points within 48 blocks of spawn at sprint speed (5.6 blocks a second);
- `explore`: flies straight outwards, by default at about 22 blocks a second, as fast as the server sends
chunks.
Machine: the ThinkPad (Intel Core i7-7500U, 2 cores / 4 threads, 32 GB, NixOS), servers under `nice -n 19`
because it also runs other services; vanilla on OpenJDK 25.0.4.1 with a 4 GB heap (`-Xms4g -Xmx4g`). The
bots ran on the Mac mini and reached the ThinkPad over the tailnet, so they took none of the server's CPU.
What ran: Horizon from `origin/main` at bf851bfe with #586 (move checks and chunk pacing), the bots and
`server_bench.py` from this PR, #623 (sections encoded once, its first commit) and #649 (a tick's packets
handed to each connection together). The ThinkPad copy wasn't a git checkout, so the JSON reports have an
empty `commit`. Vanilla: the pinned 26.3 server jar.
## Results
MSPT is the average milliseconds per tick (50 is the limit for 20 TPS); P95 is the 95th percentile. CPU is
the whole process tree. "New chunks/s" are chunks no bot had before.
### Walk (the same work on both servers)
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|---:|---|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 5.5 | 12.1 | 20.0 | 1.37 GB | 57 % | 9 | 0.3 |
| 1 | Vanilla | 18.3 | 27.3 | 20.0 | 3.35 GB | 66 % | 10 | 0.3 |
| 10 | Horizon | 11.5 | 22.1 | 20.0 | 1.41 GB | 124 % | 94 | 2.4 |
| 10 | Vanilla | 16.8 | 30.5 | 20.0 | 3.41 GB | 84 % | 100 | 2.8 |
| 25 | Horizon | 20.7 | 40.4 | 20.0 | 1.45 GB | 170 % | 236 | 5.7 |
| 25 | Vanilla | 16.0 | 26.4 | 20.0 | 3.47 GB | 115 % | 252 | 6.7 |
| 50 | Horizon | 31.4 | 79.0 | 20.0 | 1.52 GB | 173 % | 451 | 10.3 |
| 50 | Vanilla | 20.5 | 29.7 | 20.0 | 3.55 GB | 194 % | 502 | 12.2 |
| 100 | Horizon | 114.5 | 256.2 | 8.7 | 1.33 GB | 181 % | 484 | 30.8 |
| 100 | Vanilla | stopped by its watchdog | | | | | | |
Both servers sent the same chunks (about 10 a second per bot, the churn of flying around) and sent none
twice. Vanilla pulled back a few bots for moving too quickly when a tick ran long (5 to 33 per run);
Horizon none. At 100 bots vanilla's watchdog stopped the server (a tick ran over 60 s, in
`ChunkMap$TrackedEntity.updatePlayer`); Horizon kept running at 8.7 TPS.
### Explore at a rate both servers keep up with (equal work)
The bots fly outwards at 0.8 blocks a second (`--explore-speed 0.8`), slow enough that neither server makes
them wait for terrain, so both generate and send the same new chunks.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|---:|---|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 4.0 | 6.6 | 20.0 | 1.36 GB | 25 % | 2 | 1.3 |
| 1 | Vanilla | 18.1 | 27.1 | 20.0 | 3.32 GB | 73 % | 2 | 1.4 |
| 10 | Horizon | 13.5 | 24.8 | 20.0 | 1.64 GB | 89 % | 13 | 11.8 |
| 10 | Vanilla | 29.2 | 43.0 | 20.0 | 3.52 GB | 143 % | 13 | 11.9 |
| 25 | Horizon | 20.2 | 36.8 | 20.0 | 1.74 GB | 121 % | 32 | 28.9 |
| 25 | Vanilla | 37.0 | 49.4 | 20.0 | 3.73 GB | 169 % | 32 | 28.9 |
### Explore as fast as each server sends chunks (not equal work)
The default 22 blocks a second is faster than either server can generate, so each server sets the pace and
a faster server is given more work: from 10 bots Horizon generated 1.5 to 2 times vanilla's new chunks a
second. Read it as throughput, not as a like-for-like tick time.
| Bots | Server | MSPT | P95 | TPS | RSS | CPU | Chunks/s | New chunks/s |
|---:|---|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 10.6 | 24.6 | 20.0 | 1.34 GB | 322 % | 39 | 34.6 |
| 1 | Vanilla | 9.9 | 21.3 | 20.0 | 3.48 GB | 245 % | 36 | 33.4 |
| 10 | Horizon | 30.4 | 52.3 | 20.0 | 2.18 GB | 345 % | 47 | 46.7 |
| 10 | Vanilla | 17.0 | 29.6 | 20.0 | 4.04 GB | 242 % | 22 | 22.4 |
| 25 | Horizon | 50.7 | 89.3 | 19.7 | 2.30 GB | 286 % | 50 | 49.7 |
| 25 | Vanilla | 24.2 | 36.5 | 20.0 | 4.40 GB | 320 % | 34 | 33.7 |
| 50 | Horizon | 50.7 | 102.7 | 19.7 | 1.72 GB | 213 % | 97 | 96.5 |
| 50 | Vanilla | stopped by its watchdog | | | | | | |
| 100 | Horizon | 92.9 | 139.6 | 10.8 | 1.70 GB | 322 % | 114 | 113.6 |
| 100 | Vanilla | stopped by its watchdog | | | | | | |
Vanilla's watchdog stopped it at 50 bots (in `ClientboundLightUpdatePacketData`, copying light for a chunk
packet) and at 100 (in `Entity.move` from a move packet).
## Findings
- Horizon's RSS is 1.9 to 2.6 times smaller in every run, and it starts in 11 s against vanilla's 15 s.
Vanilla's heap is fixed at 4 GB (`-Xms4g`), so part of its RSS is that setting rather than need.
- With 1 bot Horizon's tick is 3.3 times shorter walking and 4.5 times shorter exploring at equal work;
at 10 bots it is still shorter in both.
- At equal exploring work Horizon's tick is about half of vanilla's at 10 and 25 bots.
- When the bots only fly around spawn (`walk`), vanilla's tick hardly grows with players (16 to 20 ms from
1 to 50 bots) while Horizon's grows by about 0.5 ms per bot, so vanilla is ahead from about 20 bots: 16.0
against 20.7 ms at 25 and 20.5 against 31.4 ms at 50. Both keep 20 TPS up to 50.
- With 100 bots vanilla's watchdog stops it in every pass, and Horizon keeps running at 9 to 11 TPS.
### Where Horizon's walk tick goes at 25 bots
Profiled on the ThinkPad with `perf` (the server thread, 20 s, self time):
| Share | What | Whose |
|---:|---|---|
| 18 % | `Streaming::update`: every tick it walks all loaded columns and proto chunks and tests each against every player's area (unloading, dropping protos, finishing and decoration candidates), so the cost grows with players times chunks | streaming / engine tick |
| 20 % | `PalettedContainer::get` and `World::block`, mostly from block entity ticks: on the Mac profile `tick_hoppers` looks up the block under every block entity in the loaded world each tick and compares names as strings, and spawners rebuild their display id | engine tick |
| 8 % | `light_data` when a chunk is sent: the light layers were built twice per chunk; a follow-up commit on #623 builds them once and keeps each section's packed layers | server (this work) |
| 7 % | encoding chunk sections and heightmaps for the chunks each bot newly sees | server |
| 4 % | `Hub::tick` (moves, tracking) | server |
| 2 % | the moved-wrongly collision check | server |
The first two grow with the number of players but aren't specific to the server: they are the
engine's tick and streaming, reported to perf-server-2. Before #623 and #649, encoding every chunk section
once per player and waking each connection's writer thread once per packet cost more than everything above
(at 50 walking bots the tick was 281 ms; see the earlier runs).
## Earlier runs (same machine and method)
- `run-1-before-chunk-cache.json`: before #623, with vanilla's locator bar on. Horizon's walk tick was
27.5, 40.7, 281 and 467 ms at 10, 25, 50 and 100 bots, and vanilla's 33.2 ms at 50 (the locator bar).
- `run-2-chunk-cache.json`: Horizon with #623 only: 13.2, 23.5, 31.4 and 84.4 ms walking at 10 to 100
bots.
## Files
- `horizon.json`, `vanilla.json`: the walk and explore passes (vanilla with `locator_bar=false`).
- `explore-equal-rate.json`: the explore pass at 0.8 blocks a second, both servers.
- `run-1-before-chunk-cache.json`, `run-2-chunk-cache.json`: the earlier runs.
To run it again (bots on another machine reached at `--connect`):
cargo build --release -p horizon-client --bin horizon-client -p horizon-server -p horizon-bots
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50,100 --behaviour walk,explore \
--gamerule locator_bar=false --nice 19 --bots-via 'ssh macmini' --bots-bin /path/to/horizon-bots \
--connect <this machine's address> --out results.json
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25 --behaviour explore \
--explore-speed 0.8 --gamerule locator_bar=false --nice 19 ... --out explore-equal-rate.json
@@ -1,267 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T05:47:12+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25",
"behaviour": "explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "locator_bar=false",
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench5.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 10.91,
"mspt": {
"average": 4.02,
"p50": 3.75,
"p95": 6.63,
"p99": 14.55
},
"mspt_worst_p99": 63.3,
"tps": 20.0,
"rss_mb": {
"mean": 1392.42,
"max": 1746.1
},
"cpu_percent": 24.52,
"chunks_per_second": 1.69,
"unique_per_second": 1.34,
"forgotten_per_second": 1.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 1048,
"chunks_per_bot": {
"explore": 1048.0
},
"unique_chunks_per_bot": {
"explore": 952.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.13,
"mspt": {
"average": 18.12,
"p50": 18.78,
"p95": 27.07,
"p99": 60.81
},
"mspt_worst_p99": 1397.6,
"tps": 20.0,
"rss_mb": {
"mean": 3397.31,
"max": 3430.2
},
"cpu_percent": 72.99,
"chunks_per_second": 1.69,
"unique_per_second": 1.38,
"forgotten_per_second": 1.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 1048,
"chunks_per_bot": {
"explore": 1048.0
},
"unique_chunks_per_bot": {
"explore": 952.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 13.49,
"p50": 12.09,
"p95": 24.81,
"p99": 40.9
},
"mspt_worst_p99": 101.4,
"tps": 20.0,
"rss_mb": {
"mean": 1684.04,
"max": 2660.6
},
"cpu_percent": 89.04,
"chunks_per_second": 13.08,
"unique_per_second": 11.84,
"forgotten_per_second": 13.02,
"waited_ticks_per_second": 0.0,
"chunks_total": 9277,
"chunks_per_bot": {
"explore": 927.7
},
"unique_chunks_per_bot": {
"explore": 889.9
},
"resent": 0,
"pulled_back": 3381,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 14.84,
"mspt": {
"average": 29.18,
"p50": 26.76,
"p95": 43.02,
"p99": 80.16
},
"mspt_worst_p99": 425.5,
"tps": 20.0,
"rss_mb": {
"mean": 3609.51,
"max": 3791.6
},
"cpu_percent": 142.79,
"chunks_per_second": 13.1,
"unique_per_second": 11.88,
"forgotten_per_second": 13.12,
"waited_ticks_per_second": 0.0,
"chunks_total": 9245,
"chunks_per_bot": {
"explore": 924.5
},
"unique_chunks_per_bot": {
"explore": 885.3
},
"resent": 0,
"pulled_back": 3388,
"moved_too_quickly": 6,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.02,
"mspt": {
"average": 20.25,
"p50": 18.73,
"p95": 36.78,
"p99": 55.76
},
"mspt_worst_p99": 92.3,
"tps": 20.0,
"rss_mb": {
"mean": 1782.26,
"max": 2741.9
},
"cpu_percent": 120.8,
"chunks_per_second": 32.22,
"unique_per_second": 28.89,
"forgotten_per_second": 32.19,
"waited_ticks_per_second": 0.0,
"chunks_total": 23357,
"chunks_per_bot": {
"explore": 934.28
},
"unique_chunks_per_bot": {
"explore": 890.32
},
"resent": 0,
"pulled_back": 11580,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.54,
"mspt": {
"average": 37.04,
"p50": 34.77,
"p95": 49.42,
"p99": 101.67
},
"mspt_worst_p99": 735.2,
"tps": 20.0,
"rss_mb": {
"mean": 3815.46,
"max": 4202.0
},
"cpu_percent": 169.15,
"chunks_per_second": 32.34,
"unique_per_second": 28.93,
"forgotten_per_second": 32.31,
"waited_ticks_per_second": 0.0,
"chunks_total": 23306,
"chunks_per_bot": {
"explore": 932.24
},
"unique_chunks_per_bot": {
"explore": 886.96
},
"resent": 0,
"pulled_back": 11539,
"moved_too_quickly": 6,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
}
]
}
@@ -1,415 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T05:04:57+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"explore_speed": 0,
"gamerule": "locator_bar=false",
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench4.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 11.02,
"mspt": {
"average": 5.45,
"p50": 4.63,
"p95": 12.14,
"p99": 30.63
},
"mspt_worst_p99": 91.5,
"tps": 20.0,
"rss_mb": {
"mean": 1398.5,
"max": 1708.4
},
"cpu_percent": 57.03,
"chunks_per_second": 9.17,
"unique_per_second": 0.26,
"forgotten_per_second": 9.2,
"waited_ticks_per_second": 0.0,
"chunks_total": 3708,
"chunks_per_bot": {
"walk": 3708.0
},
"unique_chunks_per_bot": {
"walk": 694.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 11.02,
"mspt": {
"average": 11.51,
"p50": 10.6,
"p95": 22.13,
"p99": 36.92
},
"mspt_worst_p99": 71.9,
"tps": 20.0,
"rss_mb": {
"mean": 1440.2,
"max": 1832.4
},
"cpu_percent": 124.19,
"chunks_per_second": 94.04,
"unique_per_second": 2.38,
"forgotten_per_second": 93.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 37269,
"chunks_per_bot": {
"walk": 3726.9
},
"unique_chunks_per_bot": {
"walk": 688.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 10.91,
"mspt": {
"average": 20.74,
"p50": 19.27,
"p95": 40.42,
"p99": 57.12
},
"mspt_worst_p99": 102.0,
"tps": 20.0,
"rss_mb": {
"mean": 1485.4,
"max": 1871.9
},
"cpu_percent": 169.52,
"chunks_per_second": 235.81,
"unique_per_second": 5.68,
"forgotten_per_second": 235.83,
"waited_ticks_per_second": 0.0,
"chunks_total": 94456,
"chunks_per_bot": {
"walk": 3778.24
},
"unique_chunks_per_bot": {
"walk": 692.96
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 11.12,
"mspt": {
"average": 31.41,
"p50": 25.39,
"p95": 78.97,
"p99": 110.73
},
"mspt_worst_p99": 192.9,
"tps": 20.0,
"rss_mb": {
"mean": 1558.34,
"max": 1915.3
},
"cpu_percent": 173.27,
"chunks_per_second": 450.85,
"unique_per_second": 10.3,
"forgotten_per_second": 451.86,
"waited_ticks_per_second": 0.0,
"chunks_total": 186885,
"chunks_per_bot": {
"walk": 3737.7
},
"unique_chunks_per_bot": {
"walk": 688.9
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 18.16,
"mspt": {
"average": 114.55,
"p50": 101.37,
"p95": 256.19,
"p99": 338.64
},
"mspt_worst_p99": 442.6,
"tps": 8.73,
"rss_mb": {
"mean": 1357.65,
"max": 1358.4
},
"cpu_percent": 181.21,
"chunks_per_second": 484.16,
"unique_per_second": 30.76,
"forgotten_per_second": 478.77,
"waited_ticks_per_second": 0.0,
"chunks_total": 212947,
"chunks_per_bot": {
"walk": 2129.47
},
"unique_chunks_per_bot": {
"walk": 583.02
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 17.45,
"mspt": {
"average": 10.62,
"p50": 9.2,
"p95": 24.61,
"p99": 38.18
},
"mspt_worst_p99": 81.1,
"tps": 20.0,
"rss_mb": {
"mean": 1372.73,
"max": 1568.7
},
"cpu_percent": 322.38,
"chunks_per_second": 39.21,
"unique_per_second": 34.6,
"forgotten_per_second": 38.55,
"waited_ticks_per_second": 0.51,
"chunks_total": 12549,
"chunks_per_bot": {
"explore": 12549.0
},
"unique_chunks_per_bot": {
"explore": 11168.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.32,
"mspt": {
"average": 30.38,
"p50": 27.84,
"p95": 52.31,
"p99": 77.89
},
"mspt_worst_p99": 158.3,
"tps": 20.0,
"rss_mb": {
"mean": 2231.56,
"max": 4022.6
},
"cpu_percent": 344.99,
"chunks_per_second": 47.34,
"unique_per_second": 46.73,
"forgotten_per_second": 44.57,
"waited_ticks_per_second": 112.27,
"chunks_total": 19870,
"chunks_per_bot": {
"explore": 1987.0
},
"unique_chunks_per_bot": {
"explore": 1966.5
},
"resent": 0,
"pulled_back": 16130,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.42,
"mspt": {
"average": 50.67,
"p50": 47.61,
"p95": 89.3,
"p99": 119.15
},
"mspt_worst_p99": 222.3,
"tps": 19.73,
"rss_mb": {
"mean": 2358.4,
"max": 4273.7
},
"cpu_percent": 286.32,
"chunks_per_second": 50.39,
"unique_per_second": 49.67,
"forgotten_per_second": 47.45,
"waited_ticks_per_second": 311.85,
"chunks_total": 28954,
"chunks_per_bot": {
"explore": 1158.16
},
"unique_chunks_per_bot": {
"explore": 1147.56
},
"resent": 0,
"pulled_back": 36319,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 20.06,
"mspt": {
"average": 50.69,
"p50": 47.48,
"p95": 102.67,
"p99": 133.27
},
"mspt_worst_p99": 274.4,
"tps": 19.73,
"rss_mb": {
"mean": 1764.01,
"max": 2542.0
},
"cpu_percent": 212.83,
"chunks_per_second": 97.33,
"unique_per_second": 96.47,
"forgotten_per_second": 76.74,
"waited_ticks_per_second": 750.49,
"chunks_total": 38538,
"chunks_per_bot": {
"explore": 770.76
},
"unique_chunks_per_bot": {
"explore": 763.84
},
"resent": 0,
"pulled_back": 39880,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 11.82,
"mspt": {
"average": 92.93,
"p50": 87.84,
"p95": 139.6,
"p99": 178.38
},
"mspt_worst_p99": 272.9,
"tps": 10.76,
"rss_mb": {
"mean": 1736.38,
"max": 1855.7
},
"cpu_percent": 321.53,
"chunks_per_second": 113.89,
"unique_per_second": 113.56,
"forgotten_per_second": 101.68,
"waited_ticks_per_second": 1179.4,
"chunks_total": 84617,
"chunks_per_bot": {
"explore": 846.17
},
"unique_chunks_per_bot": {
"explore": 845.01
},
"resent": 0,
"pulled_back": 128104,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 58
}
]
}
@@ -1,693 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T00:37:35+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 11.12,
"mspt": {
"average": 7.63,
"p50": 5.51,
"p95": 17.46,
"p99": 36.79
},
"mspt_worst_p99": 79.6,
"tps": 20.0,
"rss_mb": {
"mean": 1391.94,
"max": 1700.7
},
"cpu_percent": 60.18,
"chunks_per_second": 9.15,
"unique_per_second": 0.29,
"forgotten_per_second": 9.18,
"waited_ticks_per_second": 0.0,
"chunks_total": 3682,
"chunks_per_bot": {
"walk": 3682.0
},
"unique_chunks_per_bot": {
"walk": 692.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 15.35,
"mspt": {
"average": 18.08,
"p50": 18.66,
"p95": 28.05,
"p99": 46.94
},
"mspt_worst_p99": 456.4,
"tps": 20.0,
"rss_mb": {
"mean": 3455.72,
"max": 3471.7
},
"cpu_percent": 65.67,
"chunks_per_second": 9.89,
"unique_per_second": 0.29,
"forgotten_per_second": 9.92,
"waited_ticks_per_second": 0.0,
"chunks_total": 3827,
"chunks_per_bot": {
"walk": 3827.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 11.22,
"mspt": {
"average": 27.53,
"p50": 24.93,
"p95": 53.93,
"p99": 77.03
},
"mspt_worst_p99": 111.4,
"tps": 20.0,
"rss_mb": {
"mean": 1433.85,
"max": 1831.9
},
"cpu_percent": 138.65,
"chunks_per_second": 95.98,
"unique_per_second": 2.38,
"forgotten_per_second": 95.78,
"waited_ticks_per_second": 0.0,
"chunks_total": 38041,
"chunks_per_bot": {
"walk": 3804.1
},
"unique_chunks_per_bot": {
"walk": 690.8
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 14.83,
"mspt": {
"average": 18.92,
"p50": 16.02,
"p95": 39.0,
"p99": 56.13
},
"mspt_worst_p99": 180.3,
"tps": 20.0,
"rss_mb": {
"mean": 3479.45,
"max": 3535.7
},
"cpu_percent": 99.39,
"chunks_per_second": 99.93,
"unique_per_second": 2.94,
"forgotten_per_second": 100.04,
"waited_ticks_per_second": 0.0,
"chunks_total": 38411,
"chunks_per_bot": {
"walk": 3841.1
},
"unique_chunks_per_bot": {
"walk": 697.1
},
"resent": 0,
"pulled_back": 7,
"moved_too_quickly": 7,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 11.52,
"mspt": {
"average": 40.69,
"p50": 37.28,
"p95": 81.16,
"p99": 108.42
},
"mspt_worst_p99": 149.9,
"tps": 20.0,
"rss_mb": {
"mean": 1444.12,
"max": 1882.6
},
"cpu_percent": 176.94,
"chunks_per_second": 244.11,
"unique_per_second": 6.51,
"forgotten_per_second": 243.96,
"waited_ticks_per_second": 0.0,
"chunks_total": 95219,
"chunks_per_bot": {
"walk": 3808.76
},
"unique_chunks_per_bot": {
"walk": 695.64
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 15.23,
"mspt": {
"average": 18.81,
"p50": 17.26,
"p95": 30.16,
"p99": 53.9
},
"mspt_worst_p99": 390.0,
"tps": 20.0,
"rss_mb": {
"mean": 3517.36,
"max": 3555.7
},
"cpu_percent": 148.52,
"chunks_per_second": 252.34,
"unique_per_second": 7.75,
"forgotten_per_second": 251.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 96645,
"chunks_per_bot": {
"walk": 3865.8
},
"unique_chunks_per_bot": {
"walk": 698.4
},
"resent": 0,
"pulled_back": 11,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 12.72,
"mspt": {
"average": 281.42,
"p50": 278.28,
"p95": 343.64,
"p99": 384.23
},
"mspt_worst_p99": 475.1,
"tps": 3.55,
"rss_mb": {
"mean": 1334.32,
"max": 1335.4
},
"cpu_percent": 197.11,
"chunks_per_second": 391.5,
"unique_per_second": 21.24,
"forgotten_per_second": 388.33,
"waited_ticks_per_second": 0.0,
"chunks_total": 153136,
"chunks_per_bot": {
"walk": 3062.72
},
"unique_chunks_per_bot": {
"walk": 661.82
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 15.74,
"mspt": {
"average": 33.21,
"p50": 29.9,
"p95": 53.37,
"p99": 109.14
},
"mspt_worst_p99": 833.3,
"tps": 20.0,
"rss_mb": {
"mean": 3568.07,
"max": 3594.9
},
"cpu_percent": 266.59,
"chunks_per_second": 496.3,
"unique_per_second": 18.64,
"forgotten_per_second": 495.77,
"waited_ticks_per_second": 0.0,
"chunks_total": 191965,
"chunks_per_bot": {
"walk": 3839.3
},
"unique_chunks_per_bot": {
"walk": 696.22
},
"resent": 0,
"pulled_back": 184,
"moved_too_quickly": 183,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 11.22,
"mspt": {
"average": 467.37,
"p50": 464.62,
"p95": 524.07,
"p99": 548.77
},
"mspt_worst_p99": 581.1,
"tps": 2.14,
"rss_mb": {
"mean": 1353.68,
"max": 1354.9
},
"cpu_percent": 191.96,
"chunks_per_second": 377.62,
"unique_per_second": 55.8,
"forgotten_per_second": 348.88,
"waited_ticks_per_second": 0.0,
"chunks_total": 165719,
"chunks_per_bot": {
"walk": 1657.19
},
"unique_chunks_per_bot": {
"walk": 549.8
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 56
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 14.84,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 17.07,
"p50": 16.6,
"p95": 32.82,
"p99": 50.3
},
"mspt_worst_p99": 113.9,
"tps": 20.0,
"rss_mb": {
"mean": 1391.72,
"max": 1566.6
},
"cpu_percent": 333.8,
"chunks_per_second": 39.42,
"unique_per_second": 34.77,
"forgotten_per_second": 39.34,
"waited_ticks_per_second": 0.0,
"chunks_total": 13860,
"chunks_per_bot": {
"explore": 13860.0
},
"unique_chunks_per_bot": {
"explore": 12379.0
},
"resent": 0,
"pulled_back": 192,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 16.03,
"mspt": {
"average": 9.9,
"p50": 8.04,
"p95": 20.41,
"p99": 63.62
},
"mspt_worst_p99": 351.1,
"tps": 20.0,
"rss_mb": {
"mean": 3709.92,
"max": 3776.2
},
"cpu_percent": 246.64,
"chunks_per_second": 36.06,
"unique_per_second": 33.33,
"forgotten_per_second": 35.73,
"waited_ticks_per_second": 0.0,
"chunks_total": 12369,
"chunks_per_bot": {
"explore": 12369.0
},
"unique_chunks_per_bot": {
"explore": 11559.0
},
"resent": 0,
"pulled_back": 222,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.42,
"mspt": {
"average": 33.72,
"p50": 31.08,
"p95": 61.07,
"p99": 89.09
},
"mspt_worst_p99": 142.6,
"tps": 20.0,
"rss_mb": {
"mean": 2239.97,
"max": 4108.8
},
"cpu_percent": 350.83,
"chunks_per_second": 46.49,
"unique_per_second": 46.08,
"forgotten_per_second": 44.33,
"waited_ticks_per_second": 111.28,
"chunks_total": 19915,
"chunks_per_bot": {
"explore": 1991.5
},
"unique_chunks_per_bot": {
"explore": 1974.4
},
"resent": 0,
"pulled_back": 17516,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.15,
"mspt": {
"average": 16.25,
"p50": 13.67,
"p95": 28.92,
"p99": 115.9
},
"mspt_worst_p99": 1202.1,
"tps": 20.0,
"rss_mb": {
"mean": 4202.72,
"max": 4502.7
},
"cpu_percent": 247.47,
"chunks_per_second": 22.3,
"unique_per_second": 22.25,
"forgotten_per_second": 22.64,
"waited_ticks_per_second": 78.89,
"chunks_total": 10092,
"chunks_per_bot": {
"explore": 1009.2
},
"unique_chunks_per_bot": {
"explore": 1007.4
},
"resent": 0,
"pulled_back": 16286,
"moved_too_quickly": 12,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.82,
"mspt": {
"average": 48.03,
"p50": 44.99,
"p95": 83.06,
"p99": 109.63
},
"mspt_worst_p99": 191.8,
"tps": 20.0,
"rss_mb": {
"mean": 2214.16,
"max": 4598.6
},
"cpu_percent": 356.03,
"chunks_per_second": 64.2,
"unique_per_second": 63.5,
"forgotten_per_second": 60.9,
"waited_ticks_per_second": 301.65,
"chunks_total": 32247,
"chunks_per_bot": {
"explore": 1289.88
},
"unique_chunks_per_bot": {
"explore": 1279.4
},
"resent": 0,
"pulled_back": 42159,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.73,
"mspt": {
"average": 25.73,
"p50": 22.75,
"p95": 37.72,
"p99": 201.98
},
"mspt_worst_p99": 4051.5,
"tps": 20.0,
"rss_mb": {
"mean": 4355.9,
"max": 4983.2
},
"cpu_percent": 309.3,
"chunks_per_second": 33.33,
"unique_per_second": 33.21,
"forgotten_per_second": 34.22,
"waited_ticks_per_second": 258.68,
"chunks_total": 19426,
"chunks_per_bot": {
"explore": 777.04
},
"unique_chunks_per_bot": {
"explore": 772.92
},
"resent": 0,
"pulled_back": 53918,
"moved_too_quickly": 93,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 11.01,
"mspt": {
"average": 108.61,
"p50": 107.37,
"p95": 151.96,
"p99": 177.41
},
"mspt_worst_p99": 271.6,
"tps": 9.21,
"rss_mb": {
"mean": 1775.98,
"max": 1956.9
},
"cpu_percent": 359.7,
"chunks_per_second": 100.32,
"unique_per_second": 98.91,
"forgotten_per_second": 95.42,
"waited_ticks_per_second": 518.15,
"chunks_total": 55956,
"chunks_per_bot": {
"explore": 1119.12
},
"unique_chunks_per_bot": {
"explore": 1111.42
},
"resent": 0,
"pulled_back": 69967,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 14.74,
"error": "[Errno 32] Broken pipe"
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 244.43,
"p50": 243.58,
"p95": 308.39,
"p99": 360.12
},
"mspt_worst_p99": 470.2,
"tps": 4.09,
"rss_mb": {
"mean": 1806.85,
"max": 2084.5
},
"cpu_percent": 310.46,
"chunks_per_second": 186.87,
"unique_per_second": 182.99,
"forgotten_per_second": 161.75,
"waited_ticks_per_second": 1131.65,
"chunks_total": 100389,
"chunks_per_bot": {
"explore": 1003.89
},
"unique_chunks_per_bot": {
"explore": 991.9
},
"resent": 0,
"pulled_back": 56791,
"moved_too_quickly": 344,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 14.83,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -1,413 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T02:22:59+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench2.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "walk",
"start_seconds": 11.12,
"mspt": {
"average": 5.41,
"p50": 4.63,
"p95": 11.61,
"p99": 30.5
},
"mspt_worst_p99": 64.6,
"tps": 20.0,
"rss_mb": {
"mean": 1398.84,
"max": 1708.5
},
"cpu_percent": 56.84,
"chunks_per_second": 9.19,
"unique_per_second": 0.27,
"forgotten_per_second": 9.22,
"waited_ticks_per_second": 0.0,
"chunks_total": 3713,
"chunks_per_bot": {
"walk": 3713.0
},
"unique_chunks_per_bot": {
"walk": 694.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "walk",
"start_seconds": 10.92,
"mspt": {
"average": 13.18,
"p50": 11.74,
"p95": 26.5,
"p99": 44.82
},
"mspt_worst_p99": 87.6,
"tps": 20.0,
"rss_mb": {
"mean": 1439.83,
"max": 1831.6
},
"cpu_percent": 132.94,
"chunks_per_second": 93.87,
"unique_per_second": 2.38,
"forgotten_per_second": 93.77,
"waited_ticks_per_second": 0.0,
"chunks_total": 37240,
"chunks_per_bot": {
"walk": 3724.0
},
"unique_chunks_per_bot": {
"walk": 688.7
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "walk",
"start_seconds": 10.92,
"mspt": {
"average": 23.52,
"p50": 21.92,
"p95": 44.7,
"p99": 63.12
},
"mspt_worst_p99": 119.1,
"tps": 20.0,
"rss_mb": {
"mean": 1485.82,
"max": 1872.0
},
"cpu_percent": 173.82,
"chunks_per_second": 235.63,
"unique_per_second": 5.64,
"forgotten_per_second": 235.72,
"waited_ticks_per_second": 0.0,
"chunks_total": 94549,
"chunks_per_bot": {
"walk": 3781.96
},
"unique_chunks_per_bot": {
"walk": 692.8
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "walk",
"start_seconds": 10.91,
"mspt": {
"average": 31.36,
"p50": 26.08,
"p95": 70.6,
"p99": 92.83
},
"mspt_worst_p99": 125.5,
"tps": 20.0,
"rss_mb": {
"mean": 1556.81,
"max": 1913.0
},
"cpu_percent": 192.16,
"chunks_per_second": 468.69,
"unique_per_second": 10.43,
"forgotten_per_second": 468.78,
"waited_ticks_per_second": 0.0,
"chunks_total": 193547,
"chunks_per_bot": {
"walk": 3870.94
},
"unique_chunks_per_bot": {
"walk": 692.84
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "walk",
"start_seconds": 11.02,
"mspt": {
"average": 84.38,
"p50": 81.5,
"p95": 132.92,
"p99": 168.38
},
"mspt_worst_p99": 245.5,
"tps": 11.85,
"rss_mb": {
"mean": 1395.64,
"max": 1941.3
},
"cpu_percent": 271.28,
"chunks_per_second": 850.05,
"unique_per_second": 21.79,
"forgotten_per_second": 851.1,
"waited_ticks_per_second": 0.0,
"chunks_total": 368464,
"chunks_per_bot": {
"walk": 3684.64
},
"unique_chunks_per_bot": {
"walk": 680.58
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 1,
"behaviour": "explore",
"start_seconds": 11.02,
"mspt": {
"average": 11.63,
"p50": 10.27,
"p95": 24.52,
"p99": 40.53
},
"mspt_worst_p99": 99.9,
"tps": 20.0,
"rss_mb": {
"mean": 1413.81,
"max": 1596.1
},
"cpu_percent": 329.84,
"chunks_per_second": 39.47,
"unique_per_second": 34.81,
"forgotten_per_second": 39.42,
"waited_ticks_per_second": 0.0,
"chunks_total": 13798,
"chunks_per_bot": {
"explore": 13798.0
},
"unique_chunks_per_bot": {
"explore": 12330.0
},
"resent": 0,
"pulled_back": 185,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "explore",
"start_seconds": 11.02,
"mspt": {
"average": 28.49,
"p50": 26.2,
"p95": 48.58,
"p99": 72.04
},
"mspt_worst_p99": 125.7,
"tps": 20.0,
"rss_mb": {
"mean": 2231.06,
"max": 4077.8
},
"cpu_percent": 344.95,
"chunks_per_second": 46.31,
"unique_per_second": 45.88,
"forgotten_per_second": 44.14,
"waited_ticks_per_second": 111.95,
"chunks_total": 19722,
"chunks_per_bot": {
"explore": 1972.2
},
"unique_chunks_per_bot": {
"explore": 1957.0
},
"resent": 0,
"pulled_back": 18533,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "explore",
"start_seconds": 11.12,
"mspt": {
"average": 40.04,
"p50": 37.61,
"p95": 62.39,
"p99": 83.64
},
"mspt_worst_p99": 147.0,
"tps": 20.0,
"rss_mb": {
"mean": 2466.85,
"max": 4504.5
},
"cpu_percent": 340.13,
"chunks_per_second": 60.61,
"unique_per_second": 59.62,
"forgotten_per_second": 56.28,
"waited_ticks_per_second": 299.57,
"chunks_total": 32877,
"chunks_per_bot": {
"explore": 1315.08
},
"unique_chunks_per_bot": {
"explore": 1300.72
},
"resent": 0,
"pulled_back": 46272,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 50,
"behaviour": "explore",
"start_seconds": 11.32,
"mspt": {
"average": 61.82,
"p50": 59.21,
"p95": 90.46,
"p99": 115.58
},
"mspt_worst_p99": 320.9,
"tps": 16.18,
"rss_mb": {
"mean": 2634.54,
"max": 4883.1
},
"cpu_percent": 356.96,
"chunks_per_second": 95.26,
"unique_per_second": 94.31,
"forgotten_per_second": 85.86,
"waited_ticks_per_second": 528.08,
"chunks_total": 59251,
"chunks_per_bot": {
"explore": 1185.02
},
"unique_chunks_per_bot": {
"explore": 1173.96
},
"resent": 0,
"pulled_back": 99493,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 100,
"behaviour": "explore",
"start_seconds": 10.81,
"mspt": {
"average": 103.24,
"p50": 100.03,
"p95": 144.77,
"p99": 178.8
},
"mspt_worst_p99": 258.6,
"tps": 9.69,
"rss_mb": {
"mean": 1784.48,
"max": 1922.0
},
"cpu_percent": 336.94,
"chunks_per_second": 115.06,
"unique_per_second": 114.86,
"forgotten_per_second": 109.43,
"waited_ticks_per_second": 1127.57,
"chunks_total": 93839,
"chunks_per_bot": {
"explore": 938.39
},
"unique_chunks_per_bot": {
"explore": 935.28
},
"resent": 0,
"pulled_back": 133404,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 100,
"bots_failed": [],
"tick_queries": 58
}
]
}
@@ -1,324 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-17T03:29:30+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "vanilla",
"bots": "1,10,25,50,100",
"behaviour": "walk,explore",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"gamerule": "locator_bar=false",
"heap": "4g",
"out": "/home/ludovic/scratch/sh2-bench3.json",
"nice": 19,
"bots_via": "ssh -o BatchMode=yes macmini",
"bots_bin": "/tmp/sh2-horizon-bots",
"connect": "100.72.51.8",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "vanilla",
"bots": 1,
"behaviour": "walk",
"start_seconds": 15.43,
"mspt": {
"average": 18.32,
"p50": 19.26,
"p95": 27.34,
"p99": 45.48
},
"mspt_worst_p99": 393.3,
"tps": 20.0,
"rss_mb": {
"mean": 3428.43,
"max": 3437.7
},
"cpu_percent": 66.47,
"chunks_per_second": 9.92,
"unique_per_second": 0.29,
"forgotten_per_second": 9.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 3850,
"chunks_per_bot": {
"walk": 3850.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "walk",
"start_seconds": 14.76,
"mspt": {
"average": 16.8,
"p50": 14.55,
"p95": 30.52,
"p99": 50.01
},
"mspt_worst_p99": 205.2,
"tps": 20.0,
"rss_mb": {
"mean": 3490.15,
"max": 3580.7
},
"cpu_percent": 83.7,
"chunks_per_second": 100.01,
"unique_per_second": 2.83,
"forgotten_per_second": 100.04,
"waited_ticks_per_second": 0.0,
"chunks_total": 38623,
"chunks_per_bot": {
"walk": 3862.3
},
"unique_chunks_per_bot": {
"walk": 697.2
},
"resent": 0,
"pulled_back": 5,
"moved_too_quickly": 5,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "walk",
"start_seconds": 14.84,
"mspt": {
"average": 16.01,
"p50": 14.55,
"p95": 26.39,
"p99": 51.26
},
"mspt_worst_p99": 397.8,
"tps": 20.0,
"rss_mb": {
"mean": 3550.02,
"max": 3593.5
},
"cpu_percent": 114.96,
"chunks_per_second": 251.62,
"unique_per_second": 6.72,
"forgotten_per_second": 251.16,
"waited_ticks_per_second": 0.0,
"chunks_total": 98701,
"chunks_per_bot": {
"walk": 3948.04
},
"unique_chunks_per_bot": {
"walk": 698.84
},
"resent": 0,
"pulled_back": 10,
"moved_too_quickly": 10,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "walk",
"start_seconds": 14.43,
"mspt": {
"average": 20.52,
"p50": 19.55,
"p95": 29.72,
"p99": 49.04
},
"mspt_worst_p99": 600.4,
"tps": 20.0,
"rss_mb": {
"mean": 3633.12,
"max": 3658.7
},
"cpu_percent": 193.84,
"chunks_per_second": 501.86,
"unique_per_second": 12.15,
"forgotten_per_second": 501.8,
"waited_ticks_per_second": 0.0,
"chunks_total": 203429,
"chunks_per_bot": {
"walk": 4068.58
},
"unique_chunks_per_bot": {
"walk": 698.66
},
"resent": 0,
"pulled_back": 33,
"moved_too_quickly": 32,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "walk",
"start_seconds": 15.34,
"error": "[Errno 32] Broken pipe"
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "explore",
"start_seconds": 15.55,
"mspt": {
"average": 9.93,
"p50": 8.12,
"p95": 21.33,
"p99": 60.19
},
"mspt_worst_p99": 373.0,
"tps": 20.0,
"rss_mb": {
"mean": 3564.11,
"max": 3655.0
},
"cpu_percent": 245.18,
"chunks_per_second": 35.93,
"unique_per_second": 33.35,
"forgotten_per_second": 35.64,
"waited_ticks_per_second": 0.0,
"chunks_total": 12340,
"chunks_per_bot": {
"explore": 12340.0
},
"unique_chunks_per_bot": {
"explore": 11569.0
},
"resent": 0,
"pulled_back": 261,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "explore",
"start_seconds": 15.03,
"mspt": {
"average": 16.99,
"p50": 14.46,
"p95": 29.61,
"p99": 132.18
},
"mspt_worst_p99": 1244.0,
"tps": 20.0,
"rss_mb": {
"mean": 4135.2,
"max": 4453.1
},
"cpu_percent": 242.32,
"chunks_per_second": 22.39,
"unique_per_second": 22.41,
"forgotten_per_second": 22.78,
"waited_ticks_per_second": 78.78,
"chunks_total": 10177,
"chunks_per_bot": {
"explore": 1017.7
},
"unique_chunks_per_bot": {
"explore": 1016.2
},
"resent": 0,
"pulled_back": 17314,
"moved_too_quickly": 19,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "explore",
"start_seconds": 14.84,
"mspt": {
"average": 24.19,
"p50": 21.22,
"p95": 36.51,
"p99": 174.82
},
"mspt_worst_p99": 1532.8,
"tps": 20.0,
"rss_mb": {
"mean": 4504.55,
"max": 4988.9
},
"cpu_percent": 319.55,
"chunks_per_second": 33.78,
"unique_per_second": 33.66,
"forgotten_per_second": 34.64,
"waited_ticks_per_second": 269.22,
"chunks_total": 20522,
"chunks_per_bot": {
"explore": 820.88
},
"unique_chunks_per_bot": {
"explore": 815.76
},
"resent": 0,
"pulled_back": 46246,
"moved_too_quickly": 28,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "explore",
"start_seconds": 14.73,
"error": "[Errno 32] Broken pipe"
},
{
"server": "vanilla",
"bots": 100,
"behaviour": "explore",
"start_seconds": 14.54,
"error": "[Errno 32] Broken pipe"
}
]
}
@@ -1,167 +0,0 @@
# Horizon's server against vanilla 26.3, Paper and Fabric + Lithium: mixed bots, equal work (2026-10-19)
This is the same load as `../server-equal-mixed-2026-10-18/`, now with two more servers: Paper, and vanilla on
Fabric with Lithium. It also uses a newer Horizon release (v2026.10.19). That release spawns mobs around every
player (#950) and ticks blocks around every player (#931), as vanilla does. The 2026-10-18 build did neither for
players away from spawn, so it had less work to do than vanilla, and its tick times there were lower than
they would have been with equal work. These numbers replace them.
## Summary
- **The work was equal** at every count, on all four servers. Chunks sent a second and first-time chunks a
second were within 4 % of each other, and the walking bots got about the same chunks each (6050 to 6510).
Every bot joined and none was dropped. Vanilla's 50-bot server is the exception: its watchdog stopped it.
- **Memory:** Horizon used 0.72 GB with one player and 1.11 GB with 50. The three Java servers used 3.4 to
4.5 GB (a 4 GB heap each), so Horizon was **3.8 to 4.8 times smaller** at every count.
- **Start:** **8.2 to 8.5 s** to `Done` for Horizon, against 14.7 to 15.5 s for vanilla, 16.6 to 17.6 s for
Fabric + Lithium and 19.0 to 22.4 s for Paper.
- **CPU** (100 % is one of the machine's 4 threads): from 10 players up, Horizon used the least: 143 % at 50,
against Paper's 210 % and Fabric's 236 %. With one player Paper used less (32 % against 46 %).
- **Average tick (MSPT), where Horizon loses:**
- With 1 player, Paper (10.3 ms) and Fabric + Lithium (12.5) were faster than Horizon (13.7). Vanilla was
slowest (17.3).
- With 10, Fabric (20.2) and Paper (20.6) were ahead of Horizon (22.9), which tied vanilla (22.9).
- With 25, Paper (28.3) was ahead of Horizon (30.9), and Horizon was ahead of Fabric (31.4) and vanilla (37.4).
- With 50, Paper (46.9) and Horizon (47.8) were close. Fabric + Lithium fell to 17.2 TPS (58.3 ms), and
vanilla's watchdog stopped its server.
- **Slow ticks, where Horizon wins:** Horizon's P99 was the lowest from 10 players up (72.0 ms at 25 and 73.2
at 50, against Paper's 90.4 and 109.7). Its worst 5-second P99 was the lowest at every count (133 ms at
50, against 213 for Paper and 888 for Fabric).
So on this 4-thread laptop CPU, Horizon is not yet the fastest on average tick time. Paper is ahead at every
count: Horizon's average tick is a third longer than Paper's with one player and 2 % longer with 50. Fabric +
Lithium is ahead with 1 and 10 players.
Horizon is ahead on memory, start time, CPU from 10 players up, and the steadiness of its ticks (P99). Its
average tick is what perf-server-11 is working on.
## Results
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and P99
are means over the 5-second queries of the 300 measured seconds. "Worst P99" is the highest P99 of a single
query. RSS and CPU cover the server's process tree. "First-time chunks/s" counts chunks a bot hadn't
received before (summed over the bots). "Pulled back" counts position packets that moved a bot back.
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | First-time chunks/s | Pulled back |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | 13.70 | 19.06 | 27.38 | **43.4** | 20 | **0.72 / 0.72 GB** | 46 % | **8.3 s** | 9.94 | 0.01 | 0 |
| 1 | vanilla | 17.31 | 24.68 | 36.43 | 71.0 | 20 | 3.44 / 3.44 GB | 55 % | 14.9 s | 9.99 | 0.00 | 0 |
| 1 | Paper | **10.31** | 13.90 | 22.50 | 81.2 | 20 | 4.24 / 4.25 GB | **32 %** | 22.3 s | 9.81 | 0.02 | 0 |
| 1 | Fabric + Lithium | 12.46 | 17.69 | 32.74 | 70.1 | 20 | 3.43 / 3.43 GB | 46 % | 17.6 s | 9.98 | 0.00 | 0 |
| 10 | Horizon | 22.93 | 39.83 | 53.33 | **89.5** | 20 | **0.92 / 0.97 GB** | **67 %** | **8.2 s** | 61.21 | 1.78 | 3702 |
| 10 | vanilla | 22.94 | 36.92 | 56.48 | 196.6 | 20 | 3.71 / 3.76 GB | 101 % | 14.7 s | 61.63 | 1.84 | 3370 |
| 10 | Paper | 20.64 | 35.94 | 56.29 | 166.9 | 20 | 4.07 / 4.08 GB | 74 % | 20.5 s | 60.76 | 1.85 | 3974 |
| 10 | Fabric + Lithium | **20.18** | 37.04 | 56.08 | 217.9 | 20 | 3.73 / 3.78 GB | 94 % | 16.6 s | 61.65 | 1.84 | 4002 |
| 25 | Horizon | 30.92 | 48.25 | **72.00** | **121.1** | 20 | **1.01 / 1.12 GB** | **99 %** | **8.5 s** | 155.81 | 5.78 | 3008 |
| 25 | vanilla | 37.35 | 48.20 | 82.66 | 579.8 | 20 | 3.94 / 4.16 GB | 191 % | 15.5 s | 158.52 | 5.71 | 3762 |
| 25 | Paper | **28.30** | 61.68 | 90.40 | 124.3 | 20 | 4.16 / 4.25 GB | 124 % | 19.0 s | 156.35 | 5.72 | 4337 |
| 25 | Fabric + Lithium | 31.44 | 52.45 | 93.35 | 409.3 | 20 | 3.88 / 4.19 GB | 174 % | 17.5 s | 158.89 | 5.75 | 4194 |
| 50 | Horizon | 47.83 | 62.42 | **73.21** | **133.0** | 20 | **1.11 / 1.30 GB** | **143 %** | **8.3 s** | 302.68 | 11.02 | 8562 |
| 50 | vanilla | stopped by its watchdog | | | | | | | | | | |
| 50 | Paper | **46.86** | 61.16 | 109.72 | 213.4 | 20 | 4.35 / 4.52 GB | 210 % | 19.4 s | 310.51 | 11.16 | 7344 |
| 50 | Fabric + Lithium | 58.30 | 83.06 | 174.37 | 887.6 | 17.15 | 4.52 / 4.74 GB | 236 % | 17.5 s | 311.52 | 11.28 | 6350 |
Vanilla at 50: all 50 bots joined by 05:22:28 UTC. The server fell further and further behind ("Can't keep up!
… Running 16229ms or 324 ticks behind"), and at 05:28:17, during the warm-up, its watchdog stopped it: "A
single server tick took 60.00 seconds". That is the same as on 2026-10-18.
Chunks each bot received over the whole run, by behaviour (mean):
| Bots | Server | walk | explore | idle |
|---:|---|---:|---:|---:|
| 1 | Horizon | 6280 | | |
| 1 | vanilla | 6210 | | |
| 1 | Paper | 6049 | | |
| 1 | Fabric + Lithium | 6188 | | |
| 10 | Horizon | 6460 | 1195 | 473 |
| 10 | vanilla | 6272 | 1180 | 473 |
| 10 | Paper | 6177 | 1171 | 473 |
| 10 | Fabric + Lithium | 6291 | 1181 | 473 |
| 25 | Horizon | 6506 | 1262 | 473 |
| 25 | vanilla | 6307 | 1236 | 473 |
| 25 | Paper | 6280 | 1229 | 473 |
| 25 | Fabric + Lithium | 6357 | 1239 | 473 |
| 50 | Horizon | 6468 | 1251 | 473 |
| 50 | Paper | 6411 | 1224 | 473 |
| 50 | Fabric + Lithium | 6332 | 1228 | 473 |
## Equal work
`server_bench.py`'s work table compares, at each bot count, every server's chunks sent a second and
first-time chunks a second with the busiest server's. The work is equal when both are within 10 % (or within
a chunk a second when there are few).
| Bots | Chunks/s sent, first-time (Horizon; vanilla; Paper; Fabric + Lithium) | Equal |
|---:|---|---|
| 1 | 9.94, 0.01; 9.99, 0.00; 9.81, 0.02; 9.98, 0.00 | yes |
| 10 | 61.21, 1.78; 61.63, 1.84; 60.76, 1.85; 61.65, 1.84 | yes |
| 25 | 155.81, 5.78; 158.52, 5.71; 156.35, 5.72; 158.89, 5.75 | yes |
| 50 | 302.68, 11.02; (stopped); 310.51, 11.16; 311.52, 11.28 | yes |
Horizon pulled no bot back for moving too quickly (`moved too quickly`: 0 at every count). The Java servers
logged 0 to 46 such lines a run. All four sent position corrections ("Pulled back") at similar rates.
## Method
`tools/server_bench.py` ran each configuration on a fresh server:
- the same `server.properties` on all four: seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`, white-list off;
- `gamerule respawn_radius 0`, and otherwise each server's default game rules;
- the server waits for `Done`, the bots join one a second, warm up for 300 seconds and are measured for 300,
with `/tick query` every 5 seconds and RSS and CPU every second.
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice deterministic
per bot. All are creative and fly 24 blocks above spawn. `walk` flies to random points within 48 blocks at
5.6 blocks a second, `explore` flies straight out at 0.8 blocks a second (`--explore-speed 0.8`), and `idle`
hovers.
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS. The desktop was meant to
run it but didn't wake (`~/fleet/power wake desktop` timed out twice). No other session ran on the ThinkPad
(`load.txt`, a sample a minute: 0 other sessions throughout). The benchmark ran under `nice -n 19` and
`ionice -c3`. The bots ran on the Mac mini (M4 Pro, shared with other work at 51 to 98 % CPU, 82 % on
average) and reached the ThinkPad over the tailnet.
**What ran.**
- Horizon: release v2026.10.19 (07f2913d), the server archive `horizon-server-linux-x86_64.tar.gz` installed
with `install.sh`. `horizon-server` ran its `horizon-engine` with game files prepared from the owner's own
Minecraft.
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
- Paper: 26.3 build 205 (`26.3-205-main@a90108f`), from PaperMC's download API, checked against its SHA-256.
It ran with Paper's defaults except `connection-throttle: -1` in `bukkit.yml`. Every bot connects from one
address, and the default throttle (4 s) refused all but the first. Paperclip patched the pinned server jar
from its cache. Same Java and heap.
- Fabric + Lithium: the pinned server jar on Fabric loader 0.19.5 (`packs/fabric.lock.json`) with Lithium
`mc26.3-0.26.2-fabric` (`packs/mods.lock.json`, checked against its SHA-512). Lithium needs no Fabric API.
Same Java and heap.
The Paper and Fabric jars are dev-only downloads into `.local/server-bench/jars` and are never committed or
shipped.
**How it ran.** The first pass ran all four servers at 1 and 10 bots, but its Paper 10-bot run hit the
connection throttle (1 of 10 bots joined), and it was stopped after that. The 1-bot and 10-bot lines for
Horizon, vanilla and Fabric come from that pass, along with Paper's 1-bot line. A second pass with the
throttle off ran all four at 25 and 50 bots, then Paper at 10. All runs were on the same machine on the
same morning (02:57 to 06:02 UTC). `runs.jsonl` holds the 16 run lines used here.
`report-25-50.json` and `report-paper-10.json` are the second pass's reports. The first pass, stopped
early, wrote no report. Its lines come from its output.
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
HORIZON_BENCH_BIN=~/.local/share/horizon/current \
python3 tools/server_bench.py --servers horizon,vanilla,paper,fabric --bots 1,10,25,50 --behaviour mixed \
--explore-speed 0.8 --warmup 300 --nice 19 \
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
--out four-way.json
## Next
- One pass per configuration ran, on a 4-thread laptop CPU. A rerun on a bigger machine (the desktop's
24 threads) would show which server uses more cores best, and give vanilla at 50 a second chance.
- Horizon's average tick: where its 1-player tick goes (13.7 ms against Paper's 10.3). That work is
perf-server-11's.
- 100 bots.
@@ -1,870 +0,0 @@
2026-10-19T02:57:31Z
ludothinkpad ok: 29.0 GB free, 4 threads 1% busy, 0 sessions
desktop offline
macmini busy: 28.7 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T02:58:36Z
ludothinkpad ok: 28.3 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 26.0 GB free, 14 threads 92% busy, 1 sessions
macbook offline
2026-10-19T02:59:40Z
ludothinkpad ok: 28.3 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 96% busy, 1 sessions
macbook offline
2026-10-19T03:00:44Z
ludothinkpad ok: 28.3 GB free, 4 threads 15% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 97% busy, 1 sessions
macbook offline
2026-10-19T03:01:48Z
ludothinkpad ok: 27.8 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:02:52Z
ludothinkpad ok: 28.4 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 93% busy, 1 sessions
macbook offline
2026-10-19T03:03:56Z
ludothinkpad ok: 28.4 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini busy: 29.6 GB free, 14 threads 96% busy, 1 sessions
macbook offline
2026-10-19T03:05:00Z
ludothinkpad ok: 28.4 GB free, 4 threads 11% busy, 0 sessions
desktop offline
macmini ok: 29.4 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:06:04Z
ludothinkpad ok: 28.3 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:07:09Z
ludothinkpad ok: 28.4 GB free, 4 threads 11% busy, 0 sessions
desktop offline
macmini busy: 26.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:08:13Z
ludothinkpad busy: 26.9 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:09:17Z
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 29.5 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:10:21Z
ludothinkpad ok: 25.8 GB free, 4 threads 49% busy, 0 sessions
desktop offline
macmini ok: 25.8 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T03:11:25Z
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:12:29Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T03:13:33Z
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:14:37Z
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini ok: 28.7 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:15:41Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:16:46Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 25.0 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:17:50Z
ludothinkpad ok: 25.8 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T03:18:54Z
ludothinkpad ok: 26.7 GB free, 4 threads 71% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:19:58Z
ludothinkpad ok: 25.1 GB free, 4 threads 16% busy, 0 sessions
desktop offline
macmini busy: 25.6 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:21:02Z
ludothinkpad ok: 25.1 GB free, 4 threads 15% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:22:06Z
ludothinkpad ok: 25.1 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:23:10Z
ludothinkpad ok: 25.1 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T03:24:14Z
ludothinkpad ok: 25.1 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:25:19Z
ludothinkpad ok: 25.1 GB free, 4 threads 9% busy, 0 sessions
desktop offline
macmini busy: 28.6 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T03:26:23Z
ludothinkpad ok: 25.1 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini ok: 25.9 GB free, 14 threads 71% busy, 1 sessions
macbook offline
2026-10-19T03:27:27Z
ludothinkpad ok: 25.1 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini busy: 26.0 GB free, 14 threads 94% busy, 1 sessions
macbook offline
2026-10-19T03:28:31Z
ludothinkpad ok: 25.1 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T03:29:35Z
ludothinkpad ok: 27.5 GB free, 4 threads 36% busy, 0 sessions
desktop offline
macmini ok: 25.5 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T03:30:39Z
ludothinkpad ok: 25.8 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 26.2 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T03:31:43Z
ludothinkpad ok: 25.8 GB free, 4 threads 20% busy, 0 sessions
desktop offline
macmini busy: 26.1 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T03:32:47Z
ludothinkpad ok: 25.8 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini busy: 28.4 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T03:33:52Z
ludothinkpad ok: 25.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 84% busy, 2 sessions
macbook offline
2026-10-19T03:34:56Z
ludothinkpad ok: 25.8 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:36:00Z
ludothinkpad ok: 25.8 GB free, 4 threads 16% busy, 0 sessions
desktop offline
macmini ok: 27.4 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T03:37:04Z
ludothinkpad ok: 25.8 GB free, 4 threads 15% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T03:38:08Z
ludothinkpad ok: 25.8 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 94% busy, 1 sessions
macbook offline
2026-10-19T03:39:12Z
ludothinkpad ok: 25.8 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:40:16Z
ludothinkpad ok: 28.4 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini ok: 25.5 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T03:41:20Z
ludothinkpad ok: 28.3 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T03:42:24Z
ludothinkpad ok: 28.3 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T03:43:29Z
ludothinkpad ok: 28.3 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini busy: 25.0 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:44:33Z
ludothinkpad ok: 28.2 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T03:45:37Z
ludothinkpad ok: 28.2 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T03:46:41Z
ludothinkpad ok: 28.1 GB free, 4 threads 37% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T03:47:45Z
ludothinkpad ok: 28.1 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:48:49Z
ludothinkpad ok: 28.1 GB free, 4 threads 32% busy, 0 sessions
desktop offline
macmini ok: 26.2 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T03:49:53Z
ludothinkpad ok: 28.1 GB free, 4 threads 25% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:50:57Z
ludothinkpad busy: 26.6 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini ok: 25.1 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T03:52:01Z
ludothinkpad ok: 25.8 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini busy: 25.1 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:53:06Z
ludothinkpad ok: 25.8 GB free, 4 threads 50% busy, 0 sessions
desktop offline
macmini busy: 26.4 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:54:10Z
ludothinkpad ok: 25.7 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 25.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T03:55:14Z
ludothinkpad ok: 25.6 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini busy: 31.4 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T03:56:18Z
ludothinkpad ok: 25.6 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini busy: 31.0 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T03:57:22Z
ludothinkpad ok: 25.6 GB free, 4 threads 33% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T03:58:26Z
ludothinkpad ok: 25.5 GB free, 4 threads 45% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T03:59:30Z
ludothinkpad ok: 25.5 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:00:34Z
ludothinkpad ok: 25.5 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 25.0 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T04:01:38Z
ludothinkpad busy: 27.0 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.3 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T04:02:42Z
ludothinkpad ok: 26.2 GB free, 4 threads 19% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T04:03:47Z
ludothinkpad ok: 25.9 GB free, 4 threads 14% busy, 0 sessions
desktop offline
macmini ok: 27.9 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T04:04:51Z
ludothinkpad ok: 25.7 GB free, 4 threads 11% busy, 0 sessions
desktop offline
macmini busy: 30.0 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T04:05:55Z
ludothinkpad ok: 25.3 GB free, 4 threads 10% busy, 0 sessions
desktop offline
macmini busy: 27.5 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:06:59Z
ludothinkpad ok: 25.3 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:08:03Z
ludothinkpad ok: 25.3 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 25.8 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T04:09:07Z
ludothinkpad ok: 25.3 GB free, 4 threads 10% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T04:10:11Z
ludothinkpad ok: 25.3 GB free, 4 threads 8% busy, 0 sessions
desktop offline
macmini busy: 26.6 GB free, 14 threads 92% busy, 1 sessions
macbook offline
2026-10-19T04:11:15Z
ludothinkpad ok: 25.3 GB free, 4 threads 9% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:12:20Z
ludothinkpad busy: 27.5 GB free, 4 threads 88% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T04:13:24Z
ludothinkpad ok: 25.8 GB free, 4 threads 44% busy, 0 sessions
desktop offline
macmini busy: 27.6 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T04:14:28Z
ludothinkpad ok: 25.7 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T04:15:32Z
ludothinkpad ok: 25.7 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T04:16:36Z
ludothinkpad ok: 25.6 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini ok: 29.3 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T04:17:40Z
ludothinkpad ok: 25.6 GB free, 4 threads 32% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T04:18:44Z
ludothinkpad ok: 25.5 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini ok: 28.4 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T04:19:48Z
ludothinkpad ok: 25.5 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini ok: 30.4 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T04:20:52Z
ludothinkpad ok: 25.5 GB free, 4 threads 26% busy, 0 sessions
desktop offline
macmini ok: 28.1 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T04:21:57Z
ludothinkpad ok: 25.5 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini ok: 31.0 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T04:23:01Z
ludothinkpad ok: 25.5 GB free, 4 threads 10% busy, 0 sessions
desktop offline
macmini ok: 31.7 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:24:05Z
ludothinkpad ok: 28.3 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 71% busy, 1 sessions
macbook offline
2026-10-19T04:25:15Z
ludothinkpad ok: 28.8 GB free, 4 threads 4% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 98% busy, 1 sessions
macbook offline
2026-10-19T04:26:19Z
ludothinkpad ok: 28.3 GB free, 4 threads 40% busy, 0 sessions
desktop offline
macmini ok: 32.1 GB free, 14 threads 65% busy, 1 sessions
macbook offline
2026-10-19T04:27:23Z
ludothinkpad ok: 28.2 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 29.4 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T04:28:27Z
ludothinkpad ok: 28.2 GB free, 4 threads 44% busy, 0 sessions
desktop offline
macmini ok: 28.9 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:29:32Z
ludothinkpad ok: 28.1 GB free, 4 threads 33% busy, 0 sessions
desktop offline
macmini ok: 25.3 GB free, 14 threads 71% busy, 1 sessions
macbook offline
2026-10-19T04:30:36Z
ludothinkpad ok: 28.1 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 30.0 GB free, 14 threads 63% busy, 1 sessions
macbook offline
2026-10-19T04:31:40Z
ludothinkpad ok: 28.0 GB free, 4 threads 40% busy, 0 sessions
desktop offline
macmini ok: 27.4 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T04:32:44Z
ludothinkpad ok: 28.0 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 29.3 GB free, 14 threads 51% busy, 1 sessions
macbook offline
2026-10-19T04:33:48Z
ludothinkpad ok: 28.0 GB free, 4 threads 31% busy, 0 sessions
desktop offline
macmini ok: 27.5 GB free, 14 threads 67% busy, 1 sessions
macbook offline
2026-10-19T04:34:52Z
ludothinkpad ok: 28.0 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T04:35:56Z
ludothinkpad ok: 27.9 GB free, 4 threads 34% busy, 0 sessions
desktop offline
macmini ok: 28.9 GB free, 14 threads 66% busy, 1 sessions
macbook offline
2026-10-19T04:37:00Z
ludothinkpad busy: 25.8 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 75% busy, 1 sessions
macbook offline
2026-10-19T04:38:04Z
ludothinkpad ok: 25.7 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 25.6 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T04:39:09Z
ludothinkpad ok: 25.6 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T04:40:13Z
ludothinkpad ok: 25.5 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 70% busy, 1 sessions
macbook offline
2026-10-19T04:41:17Z
ludothinkpad ok: 25.4 GB free, 4 threads 62% busy, 0 sessions
desktop offline
macmini ok: 26.3 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:42:21Z
ludothinkpad ok: 25.4 GB free, 4 threads 47% busy, 0 sessions
desktop offline
macmini ok: 26.2 GB free, 14 threads 68% busy, 1 sessions
macbook offline
2026-10-19T04:43:25Z
ludothinkpad ok: 25.4 GB free, 4 threads 67% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:44:29Z
ludothinkpad ok: 25.3 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 30.4 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T04:45:33Z
ludothinkpad ok: 25.2 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini ok: 25.0 GB free, 14 threads 69% busy, 1 sessions
macbook offline
2026-10-19T04:46:37Z
ludothinkpad ok: 25.1 GB free, 4 threads 48% busy, 0 sessions
desktop offline
macmini busy: 26.4 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T04:47:42Z
ludothinkpad ok: 27.5 GB free, 4 threads 69% busy, 0 sessions
desktop offline
macmini ok: 28.0 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:48:46Z
ludothinkpad ok: 25.3 GB free, 4 threads 83% busy, 0 sessions
desktop offline
macmini busy: 25.6 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T04:49:50Z
ludothinkpad ok: 25.3 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 25.3 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:50:54Z
ludothinkpad ok: 25.2 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 30.9 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T04:51:58Z
ludothinkpad ok: 25.2 GB free, 4 threads 57% busy, 0 sessions
desktop offline
macmini ok: 29.2 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T04:53:02Z
ludothinkpad ok: 25.1 GB free, 4 threads 59% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T04:54:06Z
ludothinkpad ok: 25.1 GB free, 4 threads 38% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 90% busy, 2 sessions
macbook offline
2026-10-19T04:55:10Z
ludothinkpad ok: 25.1 GB free, 4 threads 43% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T04:56:15Z
ludothinkpad ok: 25.1 GB free, 4 threads 42% busy, 0 sessions
desktop offline
macmini ok: 27.6 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T04:57:19Z
ludothinkpad ok: 25.0 GB free, 4 threads 45% busy, 0 sessions
desktop offline
macmini ok: 27.0 GB free, 14 threads 67% busy, 1 sessions
macbook offline
2026-10-19T04:58:23Z
ludothinkpad ok: 25.0 GB free, 4 threads 48% busy, 0 sessions
desktop offline
macmini ok: 27.4 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T04:59:27Z
ludothinkpad busy: 26.6 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 27.7 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T05:00:31Z
ludothinkpad ok: 25.6 GB free, 4 threads 53% busy, 0 sessions
desktop offline
macmini ok: 32.2 GB free, 14 threads 64% busy, 1 sessions
macbook offline
2026-10-19T05:01:35Z
ludothinkpad ok: 25.7 GB free, 4 threads 54% busy, 0 sessions
desktop offline
macmini ok: 26.1 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T05:02:39Z
ludothinkpad ok: 25.6 GB free, 4 threads 59% busy, 0 sessions
desktop offline
macmini ok: 25.4 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T05:03:43Z
ludothinkpad ok: 25.6 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 25.3 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:04:48Z
ludothinkpad busy: 25.0 GB free, 4 threads 96% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 79% busy, 1 sessions
macbook offline
2026-10-19T05:05:52Z
ludothinkpad ok: 25.4 GB free, 4 threads 51% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 84% busy, 1 sessions
macbook offline
2026-10-19T05:06:56Z
ludothinkpad ok: 25.4 GB free, 4 threads 46% busy, 0 sessions
desktop offline
macmini busy: 25.9 GB free, 14 threads 92% busy, 1 sessions
macbook offline
2026-10-19T05:08:00Z
ludothinkpad ok: 25.4 GB free, 4 threads 53% busy, 0 sessions
desktop offline
macmini busy: 29.0 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:09:04Z
ludothinkpad ok: 25.1 GB free, 4 threads 64% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T05:10:08Z
ludothinkpad ok: 28.6 GB free, 4 threads 28% busy, 0 sessions
desktop offline
macmini ok: 26.8 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T05:11:12Z
ludothinkpad ok: 28.2 GB free, 4 threads 48% busy, 0 sessions
desktop offline
macmini busy: 28.5 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:12:16Z
ludothinkpad ok: 28.2 GB free, 4 threads 52% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T05:13:21Z
ludothinkpad ok: 28.1 GB free, 4 threads 41% busy, 0 sessions
desktop offline
macmini busy: 25.9 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:14:25Z
ludothinkpad ok: 28.1 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:15:29Z
ludothinkpad ok: 28.0 GB free, 4 threads 50% busy, 0 sessions
desktop offline
macmini busy: 25.4 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T05:16:33Z
ludothinkpad ok: 27.9 GB free, 4 threads 47% busy, 0 sessions
desktop offline
macmini ok: 29.1 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T05:17:37Z
ludothinkpad ok: 27.9 GB free, 4 threads 61% busy, 0 sessions
desktop offline
macmini ok: 27.5 GB free, 14 threads 75% busy, 1 sessions
macbook offline
2026-10-19T05:18:41Z
ludothinkpad ok: 27.9 GB free, 4 threads 68% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T05:19:45Z
ludothinkpad ok: 27.8 GB free, 4 threads 45% busy, 0 sessions
desktop offline
macmini busy: 27.5 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T05:20:49Z
ludothinkpad ok: 27.5 GB free, 4 threads 60% busy, 0 sessions
desktop offline
macmini busy: 26.6 GB free, 14 threads 95% busy, 1 sessions
macbook offline
2026-10-19T05:21:53Z
ludothinkpad busy: 26.7 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 29.8 GB free, 14 threads 72% busy, 1 sessions
macbook offline
2026-10-19T05:22:58Z
ludothinkpad busy: 25.7 GB free, 4 threads 97% busy, 0 sessions
desktop offline
macmini busy: 26.6 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T05:24:02Z
ludothinkpad busy: 25.4 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.2 GB free, 14 threads 85% busy, 1 sessions
macbook offline
2026-10-19T05:25:06Z
ludothinkpad ok: 25.3 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T05:26:10Z
ludothinkpad ok: 25.2 GB free, 4 threads 69% busy, 0 sessions
desktop offline
macmini ok: 28.8 GB free, 14 threads 74% busy, 1 sessions
macbook offline
2026-10-19T05:27:14Z
ludothinkpad ok: 25.2 GB free, 4 threads 82% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T05:28:18Z
ludothinkpad ok: 25.2 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini busy: 33.7 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:29:22Z
ludothinkpad busy: 26.4 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 33.7 GB free, 14 threads 81% busy, 1 sessions
macbook offline
2026-10-19T05:30:27Z
ludothinkpad ok: 25.0 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini busy: 31.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:31:31Z
ludothinkpad ok: 24.9 GB free, 4 threads 66% busy, 0 sessions
desktop offline
macmini ok: 29.7 GB free, 14 threads 77% busy, 1 sessions
macbook offline
2026-10-19T05:32:35Z
ludothinkpad busy: 25.0 GB free, 4 threads 94% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:33:39Z
ludothinkpad ok: 24.9 GB free, 4 threads 70% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T05:34:43Z
ludothinkpad busy: 24.9 GB free, 4 threads 88% busy, 0 sessions
desktop offline
macmini busy: 32.7 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:35:47Z
ludothinkpad ok: 24.9 GB free, 4 threads 72% busy, 0 sessions
desktop offline
macmini busy: 33.3 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T05:36:51Z
ludothinkpad ok: 24.8 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini ok: 32.6 GB free, 14 threads 83% busy, 1 sessions
macbook offline
2026-10-19T05:37:55Z
ludothinkpad ok: 24.7 GB free, 4 threads 71% busy, 0 sessions
desktop offline
macmini ok: 31.5 GB free, 14 threads 65% busy, 1 sessions
macbook offline
2026-10-19T05:39:00Z
ludothinkpad busy: 24.7 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini ok: 29.7 GB free, 14 threads 69% busy, 1 sessions
macbook offline
2026-10-19T05:40:04Z
ludothinkpad ok: 28.6 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini ok: 30.0 GB free, 14 threads 73% busy, 1 sessions
macbook offline
2026-10-19T05:41:08Z
ludothinkpad busy: 25.6 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 88% busy, 1 sessions
macbook offline
2026-10-19T05:42:12Z
ludothinkpad busy: 25.5 GB free, 4 threads 97% busy, 0 sessions
desktop offline
macmini ok: 27.6 GB free, 14 threads 80% busy, 1 sessions
macbook offline
2026-10-19T05:43:16Z
ludothinkpad busy: 25.3 GB free, 4 threads 98% busy, 0 sessions
desktop offline
macmini ok: 29.5 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T05:44:20Z
ludothinkpad ok: 25.2 GB free, 4 threads 85% busy, 0 sessions
desktop offline
macmini ok: 28.6 GB free, 14 threads 64% busy, 1 sessions
macbook offline
2026-10-19T05:45:25Z
ludothinkpad ok: 25.2 GB free, 4 threads 74% busy, 0 sessions
desktop offline
macmini ok: 26.6 GB free, 14 threads 78% busy, 1 sessions
macbook offline
2026-10-19T05:46:29Z
ludothinkpad ok: 25.2 GB free, 4 threads 68% busy, 0 sessions
desktop offline
macmini ok: 26.5 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T05:47:33Z
ludothinkpad ok: 24.5 GB free, 4 threads 84% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 90% busy, 1 sessions
macbook offline
2026-10-19T05:48:37Z
ludothinkpad busy: 24.4 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 30.6 GB free, 14 threads 61% busy, 1 sessions
macbook offline
2026-10-19T05:49:41Z
ludothinkpad ok: 24.2 GB free, 4 threads 80% busy, 0 sessions
desktop offline
macmini busy: 26.9 GB free, 14 threads 85% busy, 3 sessions
macbook offline
2026-10-19T05:50:45Z
ludothinkpad ok: 24.2 GB free, 4 threads 72% busy, 0 sessions
desktop offline
macmini busy: 25.7 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T05:51:49Z
ludothinkpad ok: 24.1 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini busy: 29.1 GB free, 14 threads 87% busy, 1 sessions
macbook offline
2026-10-19T05:52:54Z
ludothinkpad ok: 27.5 GB free, 4 threads 83% busy, 0 sessions
desktop offline
macmini ok: 26.9 GB free, 14 threads 76% busy, 1 sessions
macbook offline
2026-10-19T05:53:58Z
ludothinkpad ok: 25.8 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 82% busy, 1 sessions
macbook offline
2026-10-19T05:55:02Z
ludothinkpad ok: 25.8 GB free, 4 threads 35% busy, 0 sessions
desktop offline
macmini busy: 27.2 GB free, 14 threads 95% busy, 1 sessions
macbook offline
2026-10-19T05:56:06Z
ludothinkpad ok: 25.0 GB free, 4 threads 52% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 56% busy, 1 sessions
macbook offline
2026-10-19T05:57:10Z
ludothinkpad ok: 25.1 GB free, 4 threads 30% busy, 0 sessions
desktop offline
macmini busy: 26.2 GB free, 14 threads 91% busy, 1 sessions
macbook offline
2026-10-19T05:58:14Z
ludothinkpad ok: 25.1 GB free, 4 threads 36% busy, 0 sessions
desktop offline
macmini busy: 27.8 GB free, 14 threads 94% busy, 1 sessions
macbook offline
2026-10-19T05:59:18Z
ludothinkpad ok: 25.0 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 30.3 GB free, 14 threads 51% busy, 1 sessions
macbook offline
2026-10-19T06:00:22Z
ludothinkpad ok: 25.0 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 86% busy, 1 sessions
macbook offline
2026-10-19T06:01:26Z
ludothinkpad ok: 24.4 GB free, 4 threads 38% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 89% busy, 1 sessions
macbook offline
2026-10-19T06:02:31Z
ludothinkpad ok: 25.2 GB free, 4 threads 27% busy, 0 sessions
desktop offline
macmini ok: 29.1 GB free, 14 threads 55% busy, 1 sessions
macbook offline
@@ -1,437 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-19T05:52:44+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "38505a07b5334f3ea2c467efb4a00e5557f910f8",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"versions": {
"paper_build": 205,
"fabric_loader": "0.19.5",
"lithium": "mc26.3-0.26.2-fabric"
},
"settings": {
"servers": "horizon,vanilla,paper,fabric",
"bots": "25,50",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 300,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/sh10/out/mixed-equal-25-50.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/sh10-bench/target/release/horizon-bots",
"connect": "the ThinkPad's tailnet address",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 8.51,
"mspt": {
"average": 30.92,
"p50": 27.88,
"p95": 48.25,
"p99": 72.0
},
"mspt_worst_p99": 121.1,
"tps": 20.0,
"rss_mb": {
"mean": 1012.01,
"max": 1121.3
},
"cpu_percent": 98.57,
"chunks_per_second": 155.81,
"unique_per_second": 5.78,
"forgotten_per_second": 155.62,
"waited_ticks_per_second": 0.0,
"chunks_total": 106268,
"chunks_per_bot": {
"walk": 6506.33,
"explore": 1261.6,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 702.13,
"explore": 1184.4,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3008,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 15.47,
"mspt": {
"average": 37.35,
"p50": 35.64,
"p95": 48.2,
"p99": 82.66
},
"mspt_worst_p99": 579.8,
"tps": 20.0,
"rss_mb": {
"mean": 3935.5,
"max": 4157.4
},
"cpu_percent": 191.44,
"chunks_per_second": 158.52,
"unique_per_second": 5.71,
"forgotten_per_second": 158.39,
"waited_ticks_per_second": 0.0,
"chunks_total": 103148,
"chunks_per_bot": {
"walk": 6306.87,
"explore": 1236.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 704.2,
"explore": 1160.2,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3762,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "paper",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 18.95,
"mspt": {
"average": 28.3,
"p50": 23.7,
"p95": 61.68,
"p99": 90.4
},
"mspt_worst_p99": 124.3,
"tps": 20.0,
"rss_mb": {
"mean": 4160.34,
"max": 4246.4
},
"cpu_percent": 124.24,
"chunks_per_second": 156.35,
"unique_per_second": 5.72,
"forgotten_per_second": 156.25,
"waited_ticks_per_second": 0.0,
"chunks_total": 102715,
"chunks_per_bot": {
"walk": 6280.4,
"explore": 1228.8,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 703.07,
"explore": 1155.6,
"idle": 473.0
},
"resent": 0,
"pulled_back": 4337,
"moved_too_quickly": 9,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "fabric",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 17.45,
"mspt": {
"average": 31.44,
"p50": 28.33,
"p95": 52.45,
"p99": 93.35
},
"mspt_worst_p99": 409.3,
"tps": 20.0,
"rss_mb": {
"mean": 3884.61,
"max": 4186.4
},
"cpu_percent": 174.04,
"chunks_per_second": 158.89,
"unique_per_second": 5.75,
"forgotten_per_second": 158.54,
"waited_ticks_per_second": 0.0,
"chunks_total": 103913,
"chunks_per_bot": {
"walk": 6356.8,
"explore": 1239.2,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 704.27,
"explore": 1163.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 4194,
"moved_too_quickly": 11,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 59
},
{
"server": "horizon",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 8.31,
"mspt": {
"average": 47.83,
"p50": 46.02,
"p95": 62.42,
"p99": 73.21
},
"mspt_worst_p99": 133.0,
"tps": 20.0,
"rss_mb": {
"mean": 1112.46,
"max": 1296.4
},
"cpu_percent": 143.22,
"chunks_per_second": 302.68,
"unique_per_second": 11.02,
"forgotten_per_second": 303.15,
"waited_ticks_per_second": 0.0,
"chunks_total": 211296,
"chunks_per_bot": {
"walk": 6468.47,
"explore": 1251.2,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.67,
"explore": 1180.5,
"idle": 473.0
},
"resent": 0,
"pulled_back": 8562,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 15.45,
"error": "[Errno 32] Broken pipe"
},
{
"server": "paper",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 19.35,
"mspt": {
"average": 46.86,
"p50": 45.11,
"p95": 61.16,
"p99": 109.72
},
"mspt_worst_p99": 213.4,
"tps": 20.0,
"rss_mb": {
"mean": 4353.02,
"max": 4516.1
},
"cpu_percent": 210.21,
"chunks_per_second": 310.51,
"unique_per_second": 11.16,
"forgotten_per_second": 310.64,
"waited_ticks_per_second": 0.0,
"chunks_total": 209303,
"chunks_per_bot": {
"walk": 6411.0,
"explore": 1224.3,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 703.47,
"explore": 1155.8,
"idle": 473.0
},
"resent": 0,
"pulled_back": 7344,
"moved_too_quickly": 35,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "fabric",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 17.45,
"mspt": {
"average": 58.3,
"p50": 54.73,
"p95": 83.06,
"p99": 174.37
},
"mspt_worst_p99": 887.6,
"tps": 17.15,
"rss_mb": {
"mean": 4521.47,
"max": 4744.2
},
"cpu_percent": 236.04,
"chunks_per_second": 311.52,
"unique_per_second": 11.28,
"forgotten_per_second": 311.74,
"waited_ticks_per_second": 0.0,
"chunks_total": 206965,
"chunks_per_bot": {
"walk": 6331.7,
"explore": 1228.4,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 703.3,
"explore": 1160.3,
"idle": 473.0
},
"resent": 0,
"pulled_back": 6350,
"moved_too_quickly": 46,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 58
}
],
"work": [
{
"behaviour": "mixed",
"bots": 25,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 155.81,
"share": 0.981
},
"unique_per_second": {
"value": 5.78,
"share": 1.0
},
"waited_ticks_per_second": 0.0
},
"vanilla": {
"chunks_per_second": {
"value": 158.52,
"share": 0.998
},
"unique_per_second": {
"value": 5.71,
"share": 0.988
},
"waited_ticks_per_second": 0.0
},
"paper": {
"chunks_per_second": {
"value": 156.35,
"share": 0.984
},
"unique_per_second": {
"value": 5.72,
"share": 0.99
},
"waited_ticks_per_second": 0.0
},
"fabric": {
"chunks_per_second": {
"value": 158.89,
"share": 1.0
},
"unique_per_second": {
"value": 5.75,
"share": 0.995
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
},
{
"behaviour": "mixed",
"bots": 50,
"servers": {
"horizon": {
"chunks_per_second": {
"value": 302.68,
"share": 0.972
},
"unique_per_second": {
"value": 11.02,
"share": 0.977
},
"waited_ticks_per_second": 0.0
},
"paper": {
"chunks_per_second": {
"value": 310.51,
"share": 0.997
},
"unique_per_second": {
"value": 11.16,
"share": 0.989
},
"waited_ticks_per_second": 0.0
},
"fabric": {
"chunks_per_second": {
"value": 311.52,
"share": 1.0
},
"unique_per_second": {
"value": 11.28,
"share": 1.0
},
"waited_ticks_per_second": 0.0
}
},
"equal": true
}
]
}
@@ -1,90 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-19T06:03:34+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4
},
"commit": "38505a07b5334f3ea2c467efb4a00e5557f910f8",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"versions": {
"paper_build": 205
},
"settings": {
"servers": "paper",
"bots": "10",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 300,
"seconds": 300,
"seed": 12345,
"explore_speed": 0.8,
"gamerule": "",
"heap": "4g",
"out": "/home/ludovic/sh10/out/mixed-equal-paper-10.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "/Users/ludovicmarie/code/horizon/.claude/worktrees/sh10-bench/target/release/horizon-bots",
"connect": "the ThinkPad's tailnet address",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "paper",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 20.48,
"mspt": {
"average": 20.64,
"p50": 18.1,
"p95": 35.94,
"p99": 56.29
},
"mspt_worst_p99": 166.9,
"tps": 20.0,
"rss_mb": {
"mean": 4069.94,
"max": 4080.2
},
"cpu_percent": 73.56,
"chunks_per_second": 60.76,
"unique_per_second": 1.85,
"forgotten_per_second": 60.96,
"waited_ticks_per_second": 0.0,
"chunks_total": 40348,
"chunks_per_bot": {
"walk": 6176.67,
"explore": 1171.0,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 701.0,
"explore": 1091.5,
"idle": 473.0
},
"resent": 0,
"pulled_back": 3974,
"moved_too_quickly": 7,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 59
}
],
"work": []
}
@@ -1,16 +0,0 @@
{"server": "horizon", "bots": 1, "behaviour": "mixed", "start_seconds": 8.31, "mspt": {"average": 13.7, "p50": 14.18, "p95": 19.06, "p99": 27.38}, "mspt_worst_p99": 43.4, "tps": 20.0, "rss_mb": {"mean": 715.63, "max": 719.6}, "cpu_percent": 46.22, "chunks_per_second": 9.94, "unique_per_second": 0.01, "forgotten_per_second": 9.89, "waited_ticks_per_second": 0.0, "chunks_total": 6280, "chunks_per_bot": {"walk": 6280.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 1, "behaviour": "mixed", "start_seconds": 14.85, "mspt": {"average": 17.31, "p50": 18.3, "p95": 24.68, "p99": 36.43}, "mspt_worst_p99": 71.0, "tps": 20.0, "rss_mb": {"mean": 3442.51, "max": 3444.5}, "cpu_percent": 55.03, "chunks_per_second": 9.99, "unique_per_second": 0.0, "forgotten_per_second": 9.95, "waited_ticks_per_second": 0.0, "chunks_total": 6210, "chunks_per_bot": {"walk": 6210.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "paper", "bots": 1, "behaviour": "mixed", "start_seconds": 22.27, "mspt": {"average": 10.31, "p50": 10.4, "p95": 13.9, "p99": 22.5}, "mspt_worst_p99": 81.2, "tps": 20.0, "rss_mb": {"mean": 4239.22, "max": 4245.1}, "cpu_percent": 31.68, "chunks_per_second": 9.81, "unique_per_second": 0.02, "forgotten_per_second": 9.77, "waited_ticks_per_second": 0.0, "chunks_total": 6049, "chunks_per_bot": {"walk": 6049.0}, "unique_chunks_per_bot": {"walk": 698.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "fabric", "bots": 1, "behaviour": "mixed", "start_seconds": 17.55, "mspt": {"average": 12.46, "p50": 12.62, "p95": 17.69, "p99": 32.74}, "mspt_worst_p99": 70.1, "tps": 20.0, "rss_mb": {"mean": 3428.55, "max": 3430.2}, "cpu_percent": 45.86, "chunks_per_second": 9.98, "unique_per_second": 0.0, "forgotten_per_second": 9.93, "waited_ticks_per_second": 0.0, "chunks_total": 6188, "chunks_per_bot": {"walk": 6188.0}, "unique_chunks_per_bot": {"walk": 700.0}, "resent": 0, "pulled_back": 0, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 1, "bots_failed": [], "tick_queries": 58}
{"server": "horizon", "bots": 10, "behaviour": "mixed", "start_seconds": 8.21, "mspt": {"average": 22.93, "p50": 20.03, "p95": 39.83, "p99": 53.33}, "mspt_worst_p99": 89.5, "tps": 20.0, "rss_mb": {"mean": 922.28, "max": 973.7}, "cpu_percent": 66.6, "chunks_per_second": 61.21, "unique_per_second": 1.78, "forgotten_per_second": 61.34, "waited_ticks_per_second": 0.0, "chunks_total": 42095, "chunks_per_bot": {"walk": 6460.0, "explore": 1194.5, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.17, "explore": 1112.5, "idle": 473.0}, "resent": 0, "pulled_back": 3702, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 10, "behaviour": "mixed", "start_seconds": 14.65, "mspt": {"average": 22.94, "p50": 20.61, "p95": 36.92, "p99": 56.48}, "mspt_worst_p99": 196.6, "tps": 20.0, "rss_mb": {"mean": 3706.57, "max": 3761.6}, "cpu_percent": 100.83, "chunks_per_second": 61.63, "unique_per_second": 1.84, "forgotten_per_second": 61.68, "waited_ticks_per_second": 0.0, "chunks_total": 40938, "chunks_per_bot": {"walk": 6272.17, "explore": 1179.5, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.0, "explore": 1097.0, "idle": 473.0}, "resent": 0, "pulled_back": 3370, "moved_too_quickly": 7, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
{"server": "paper", "bots": 10, "behaviour": "mixed", "start_seconds": 20.48, "mspt": {"average": 20.64, "p50": 18.1, "p95": 35.94, "p99": 56.29}, "mspt_worst_p99": 166.9, "tps": 20.0, "rss_mb": {"mean": 4069.94, "max": 4080.2}, "cpu_percent": 73.56, "chunks_per_second": 60.76, "unique_per_second": 1.85, "forgotten_per_second": 60.96, "waited_ticks_per_second": 0.0, "chunks_total": 40348, "chunks_per_bot": {"walk": 6176.67, "explore": 1171.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.0, "explore": 1091.5, "idle": 473.0}, "resent": 0, "pulled_back": 3974, "moved_too_quickly": 7, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
{"server": "fabric", "bots": 10, "behaviour": "mixed", "start_seconds": 16.64, "mspt": {"average": 20.18, "p50": 17.63, "p95": 37.04, "p99": 56.08}, "mspt_worst_p99": 217.9, "tps": 20.0, "rss_mb": {"mean": 3732.43, "max": 3776.5}, "cpu_percent": 94.48, "chunks_per_second": 61.65, "unique_per_second": 1.84, "forgotten_per_second": 61.76, "waited_ticks_per_second": 0.0, "chunks_total": 41055, "chunks_per_bot": {"walk": 6291.17, "explore": 1181.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.83, "explore": 1098.0, "idle": 473.0}, "resent": 0, "pulled_back": 4002, "moved_too_quickly": 3, "moved_wrongly": 0, "bots_joined": 10, "bots_failed": [], "tick_queries": 59}
{"server": "horizon", "bots": 25, "behaviour": "mixed", "start_seconds": 8.51, "mspt": {"average": 30.92, "p50": 27.88, "p95": 48.25, "p99": 72.0}, "mspt_worst_p99": 121.1, "tps": 20.0, "rss_mb": {"mean": 1012.01, "max": 1121.3}, "cpu_percent": 98.57, "chunks_per_second": 155.81, "unique_per_second": 5.78, "forgotten_per_second": 155.62, "waited_ticks_per_second": 0.0, "chunks_total": 106268, "chunks_per_bot": {"walk": 6506.33, "explore": 1261.6, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 702.13, "explore": 1184.4, "idle": 473.0}, "resent": 0, "pulled_back": 3008, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 25, "behaviour": "mixed", "start_seconds": 15.47, "mspt": {"average": 37.35, "p50": 35.64, "p95": 48.2, "p99": 82.66}, "mspt_worst_p99": 579.8, "tps": 20.0, "rss_mb": {"mean": 3935.5, "max": 4157.4}, "cpu_percent": 191.44, "chunks_per_second": 158.52, "unique_per_second": 5.71, "forgotten_per_second": 158.39, "waited_ticks_per_second": 0.0, "chunks_total": 103148, "chunks_per_bot": {"walk": 6306.87, "explore": 1236.0, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 704.2, "explore": 1160.2, "idle": 473.0}, "resent": 0, "pulled_back": 3762, "moved_too_quickly": 11, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
{"server": "paper", "bots": 25, "behaviour": "mixed", "start_seconds": 18.95, "mspt": {"average": 28.3, "p50": 23.7, "p95": 61.68, "p99": 90.4}, "mspt_worst_p99": 124.3, "tps": 20.0, "rss_mb": {"mean": 4160.34, "max": 4246.4}, "cpu_percent": 124.24, "chunks_per_second": 156.35, "unique_per_second": 5.72, "forgotten_per_second": 156.25, "waited_ticks_per_second": 0.0, "chunks_total": 102715, "chunks_per_bot": {"walk": 6280.4, "explore": 1228.8, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.07, "explore": 1155.6, "idle": 473.0}, "resent": 0, "pulled_back": 4337, "moved_too_quickly": 9, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
{"server": "fabric", "bots": 25, "behaviour": "mixed", "start_seconds": 17.45, "mspt": {"average": 31.44, "p50": 28.33, "p95": 52.45, "p99": 93.35}, "mspt_worst_p99": 409.3, "tps": 20.0, "rss_mb": {"mean": 3884.61, "max": 4186.4}, "cpu_percent": 174.04, "chunks_per_second": 158.89, "unique_per_second": 5.75, "forgotten_per_second": 158.54, "waited_ticks_per_second": 0.0, "chunks_total": 103913, "chunks_per_bot": {"walk": 6356.8, "explore": 1239.2, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 704.27, "explore": 1163.0, "idle": 473.0}, "resent": 0, "pulled_back": 4194, "moved_too_quickly": 11, "moved_wrongly": 0, "bots_joined": 25, "bots_failed": [], "tick_queries": 59}
{"server": "horizon", "bots": 50, "behaviour": "mixed", "start_seconds": 8.31, "mspt": {"average": 47.83, "p50": 46.02, "p95": 62.42, "p99": 73.21}, "mspt_worst_p99": 133.0, "tps": 20.0, "rss_mb": {"mean": 1112.46, "max": 1296.4}, "cpu_percent": 143.22, "chunks_per_second": 302.68, "unique_per_second": 11.02, "forgotten_per_second": 303.15, "waited_ticks_per_second": 0.0, "chunks_total": 211296, "chunks_per_bot": {"walk": 6468.47, "explore": 1251.2, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 701.67, "explore": 1180.5, "idle": 473.0}, "resent": 0, "pulled_back": 8562, "moved_too_quickly": 0, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
{"server": "vanilla", "bots": 50, "behaviour": "mixed", "start_seconds": 15.45, "error": "[Errno 32] Broken pipe"}
{"server": "paper", "bots": 50, "behaviour": "mixed", "start_seconds": 19.35, "mspt": {"average": 46.86, "p50": 45.11, "p95": 61.16, "p99": 109.72}, "mspt_worst_p99": 213.4, "tps": 20.0, "rss_mb": {"mean": 4353.02, "max": 4516.1}, "cpu_percent": 210.21, "chunks_per_second": 310.51, "unique_per_second": 11.16, "forgotten_per_second": 310.64, "waited_ticks_per_second": 0.0, "chunks_total": 209303, "chunks_per_bot": {"walk": 6411.0, "explore": 1224.3, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.47, "explore": 1155.8, "idle": 473.0}, "resent": 0, "pulled_back": 7344, "moved_too_quickly": 35, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
{"server": "fabric", "bots": 50, "behaviour": "mixed", "start_seconds": 17.45, "mspt": {"average": 58.3, "p50": 54.73, "p95": 83.06, "p99": 174.37}, "mspt_worst_p99": 887.6, "tps": 17.15, "rss_mb": {"mean": 4521.47, "max": 4744.2}, "cpu_percent": 236.04, "chunks_per_second": 311.52, "unique_per_second": 11.28, "forgotten_per_second": 311.74, "waited_ticks_per_second": 0.0, "chunks_total": 206965, "chunks_per_bot": {"walk": 6331.7, "explore": 1228.4, "idle": 473.0}, "unique_chunks_per_bot": {"walk": 703.3, "explore": 1160.3, "idle": 473.0}, "resent": 0, "pulled_back": 6350, "moved_too_quickly": 46, "moved_wrongly": 0, "bots_joined": 50, "bots_failed": [], "tick_queries": 58}
@@ -1,125 +0,0 @@
# Horizon's server against vanilla 26.3: mixed bots (2026-10-18)
The first benchmark from docs/server.md section 10 (item 4): the same bots on Horizon's dedicated server, as
`install.sh` installs it today, and on vanilla 26.3's, one server at a time on the same quiet machine.
There were 1, 10, 25 and 50 bots, all with the `mixed` behaviour.
## Summary
- **Both servers held 20 TPS in every run.**
- **Memory:** Horizon's RSS was 3.7 to 5.2 times smaller: 0.65 GB against 3.4 GB with one player, and
1.0 GB against 4.0 GB with 50. Vanilla runs with the usual fixed heap (`-Xms4g -Xmx4g`), so its RSS
is mostly that heap.
- **Tick time (MSPT):**
- with 1, 10 and 25 players, Horizon's average tick was shorter (2.4 against 17.9 ms, 12.4 against 34.5,
24.0 against 31.6);
- with 50, it was longer (35.2 against 29.7 ms);
- Horizon's P99 was lower at every count, and so were its worst 5-second windows (241 ms at most,
against 424 to 2320 ms for vanilla).
- **The work was not equal, and the difference counts against Horizon.** In `mixed`, 20 % of the bots
fly straight out at 22 blocks a second, as fast as each server sends them chunks. Horizon kept up:
- with 10 or more players it sent 2.9 to 5.9 times as many chunks a second, and generated 1.9 to 2.9
times as much new terrain a second;
- its walking bots each received 3300 to 4000 chunks, against 640 to 1100 on vanilla once 10 or more
were online.
So at 50 players Horizon's tick is 18 % longer while it generates twice the new terrain. The
equal-rate explore pass of `../server-equal-work-2026-10-17/` is the fair comparison for exploring.
- **Start (fresh world, to `Done`):** 10 to 20 s for Horizon, 14.5 to 28.6 s for vanilla.
## Results
MSPT is milliseconds per tick from each server's own `/tick query` (50 is the limit for 20 TPS). P95 and
P99 are over the whole run. "Worst P99" is the highest P99 of a single 5-second query. RSS and CPU cover
the server's whole process tree (100 % is one thread; the machine has 4). "New chunks/s" counts chunks
no bot had before. "Pulled back" counts position packets that moved a bot back.
| Bots | Server | MSPT | P95 | P99 | Worst P99 | TPS | RSS (mean / max) | CPU | Start | Chunks/s | New chunks/s | Pulled back |
|---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 1 | Horizon | **2.4** | 5.2 | 18.2 | 45.6 | 20 | **0.65 / 0.66 GB** | 25 % | 10.9 s | 9.9 | 0.3 | 0 |
| 1 | vanilla | 17.9 | 27.6 | 47.3 | 423.8 | 20 | 3.38 / 3.39 GB | 64 % | 14.8 s | 9.9 | 0.3 | 0 |
| 10 | Horizon | **12.4** | 36.0 | 65.9 | 117.1 | 20 | **0.94 / 1.01 GB** | 210 % | 10.2 s | 98.2 | 36.5 | 6267 |
| 10 | vanilla | 34.5 | 63.5 | 138.6 | 884.7 | 20 | 3.59 / 3.66 GB | 217 % | 14.5 s | 24.3 | 19.1 | 5500 |
| 25 | Horizon | **24.0** | 49.7 | 80.6 | 168.8 | 20 | **1.01 / 1.18 GB** | 192 % | 14.7 s | 178.2 | 31.9 | 5532 |
| 25 | vanilla | 31.6 | 70.0 | 222.7 | 2319.6 | 20 | 3.74 / 4.06 GB | 218 % | 28.6 s | 30.4 | 10.8 | 4524 |
| 50 | Horizon | 35.2 | 69.0 | 96.6 | 240.8 | 20 | **1.01 / 1.25 GB** | 218 % | 19.8 s | 273.2 | 34.6 | 8369 |
| 50 | vanilla | **29.7** | 62.3 | 137.4 | 489.3 | 20 | 4.00 / 4.60 GB | 294 % | 20.6 s | 94.4 | 17.8 | 12034 |
Chunks each bot received, by behaviour (mean):
| Bots | Server | walk | explore | idle |
|---:|---|---:|---:|---:|
| 10 | Horizon | 3992 | 7360 | 473 |
| 10 | vanilla | 680 | 2824 | 358 |
| 25 | Horizon | 3928 | 2475 | 473 |
| 25 | vanilla | 640 | 738 | 180 |
| 50 | Horizon | 3300 | 1192 | 460 |
| 50 | vanilla | 1105 | 629 | 220 |
No bot failed to join or was dropped, and no chunk was sent twice (`resent` 0), on either server. Vanilla
also logged "moved too quickly" 12, 324 and 2193 times at 10, 25 and 50 bots.
## Method
`tools/server_bench.py`, unchanged, runs each configuration on a fresh server:
- the same `server.properties` on both: seed 12345, view and simulation distance 10, offline, creative,
difficulty easy, `sync-chunk-writes=true`, white-list off;
- `gamerule respawn_radius 0`, and otherwise vanilla's default game rules on both. The locator bar is on
for both: Horizon's server has run it since v2026.10.18.13. The 2026-10-17 runs turned it off on vanilla
because Horizon had none then;
- the server waits for `Done`, then the bots join one a second, warm up for 60 seconds, and are measured
for 300 seconds, with `/tick query` every 5 seconds and RSS and CPU every second.
The bots are `horizon-bots --behaviour mixed`: 60 % walk, 20 % explore and 20 % idle, each choice
deterministic per bot. All are creative and fly 24 blocks above their spawn:
- `walk`: random points within 48 blocks at 5.6 blocks a second;
- `explore`: straight out at up to 22 blocks a second, as fast as chunks come;
- `idle`: hovers.
A bot doesn't move while the chunk it stands in hasn't arrived, and it acknowledges chunk batches as
vanilla's client does.
**Machine.** The ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS (Linux 6.18). No other
session ran on it (`load.txt`: the fleet monitor shows 0 sessions throughout). The busy periods are the
servers themselves, which used 2 to 3 of its 4 threads from 10 bots up, so both servers were CPU-bound
there. The servers ran under `nice -n 19`. The desktop was meant to run this (24 threads) but didn't
wake. The bots ran on the Mac mini (M4 Pro) and reached the ThinkPad over the tailnet, so they took none
of the servers' CPU. The Mac mini was shared with other work (70 to 90 % busy), which only affects the
bots' timing.
**What ran.**
- Horizon: the published v2026.10.18.15 release, `horizon-client` (the engine) at 6c078b28, as
`install.sh` installs it. Its game files were prepared from the owner's own Minecraft by the release's
`horizon-prepare`. `horizon-server` (the supervisor, which takes no part in the tick) came from #898
(4ea2243e), because the release's copy can't find game files prepared without sounds (#893).
`horizon-bots` came from main.
- Vanilla: the pinned 26.3 server jar (the owner's own copy) on OpenJDK 25.0.4.1, `-Xms4g -Xmx4g`.
On Linux the release's engine needs `libasound.so.2` and `libudev.so.1`. The ThinkPad has them only in
the Nix store, so the run set `LD_LIBRARY_PATH` to them (see #898).
## Reproduce
curl -fsSL https://raw.githubusercontent.com/ludovic111/horizon/main/packaging/server/install.sh | sh
horizon-prepare --prepare --server-jar /path/to/server.jar # your own copy
cargo build --release -p horizon-bots # on the machine that runs the bots
# in a checkout whose .local/minecraft holds the pinned vanilla jars (tools/lab.py prepare):
HORIZON_BENCH_BIN=<a folder with horizon-server and horizon-client> \
python3 tools/server_bench.py --servers horizon,vanilla --bots 1,10,25,50 --behaviour mixed --nice 19 \
--bots-via 'ssh <bots machine>' --bots-bin /path/to/horizon-bots --connect <this machine's address> \
--out mixed.json
`mixed.json` is the report (`horizon-server-benchmark`: machine, versions, settings and every run), and
`load.txt` is the ThinkPad's load before and during the run.
## Next
- An equal-work `mixed` pass (`--explore-speed` at a rate both servers keep up with), then 100 bots.
- The same run on a bigger machine, so neither server is CPU-bound at 25 to 50 bots.
- Paper and Fabric + Lithium (docs/server.md section 9).
- At 50 players Horizon's tick is longer than vanilla's while it generates and sends far more. The
profile of the 2026-10-17 run (streaming and block entity ticks growing with players) is
perf-server's to take on.
-286
View File
@@ -1,286 +0,0 @@
ludothinkpad — OK
RAM 29.1 GB free of 31.2 GB · swap 0.3 GB/15.6 GB
CPU 4 threads, 2% busy (agents 0.0, rest 0.1 cores) · 67°C · load 2.1
· disk 295.5 GB free · battery 96%
Sessions, most important first:
(finished session) 0.0 GB leftovers: node 0.0 GB
Other machines (≤ 1 min old): desktop offline · macbook offline · macmini ok: 26.6 GB free, 14 threads 69% busy, 1 sessions
# During the run, once a minute (uptime, then ~/fleet/load fleet)
18:25:30
18:25:30 up 8 days 3:08, 0 users, load average: 2.13, 1.53, 1.29
ludothinkpad ok: 29.1 GB free, 4 threads 2% busy, 0 sessions
desktop offline
macmini busy: 27.6 GB free, 14 threads 87% busy, 1 sessions
18:26:34
18:26:34 up 8 days 3:09, 0 users, load average: 1.70, 1.56, 1.31
ludothinkpad ok: 28.5 GB free, 4 threads 23% busy, 0 sessions
desktop offline
macmini busy: 26.4 GB free, 14 threads 85% busy, 1 sessions
18:27:38
18:27:38 up 8 days 3:10, 0 users, load average: 1.10, 1.43, 1.28
ludothinkpad ok: 28.5 GB free, 4 threads 4% busy, 0 sessions
desktop offline
macmini ok: 27.7 GB free, 14 threads 80% busy, 1 sessions
18:28:42
18:28:42 up 8 days 3:11, 0 users, load average: 0.93, 1.29, 1.24
ludothinkpad ok: 28.5 GB free, 4 threads 22% busy, 0 sessions
desktop offline
macmini ok: 26.0 GB free, 14 threads 82% busy, 1 sessions
18:29:46
18:29:46 up 8 days 3:12, 0 users, load average: 0.62, 1.12, 1.18
ludothinkpad ok: 28.5 GB free, 4 threads 12% busy, 0 sessions
desktop offline
macmini ok: 25.7 GB free, 14 threads 81% busy, 1 sessions
18:30:50
18:30:51 up 8 days 3:13, 0 users, load average: 0.79, 1.05, 1.15
ludothinkpad ok: 28.5 GB free, 4 threads 7% busy, 0 sessions
desktop offline
macmini busy: 27.0 GB free, 14 threads 88% busy, 1 sessions
18:31:55
18:31:55 up 8 days 3:14, 0 users, load average: 0.67, 0.96, 1.11
ludothinkpad ok: 28.6 GB free, 4 threads 73% busy, 0 sessions
desktop offline
macmini ok: 27.3 GB free, 14 threads 79% busy, 1 sessions
18:32:59
18:32:59 up 8 days 3:15, 0 users, load average: 4.01, 1.91, 1.44
ludothinkpad ok: 25.9 GB free, 4 threads 24% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 85% busy, 1 sessions
18:34:03
18:34:03 up 8 days 3:16, 0 users, load average: 2.24, 1.84, 1.44
ludothinkpad ok: 25.9 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini busy: 29.2 GB free, 14 threads 86% busy, 1 sessions
18:35:07
18:35:07 up 8 days 3:17, 0 users, load average: 1.28, 1.63, 1.40
ludothinkpad ok: 25.8 GB free, 4 threads 21% busy, 0 sessions
desktop offline
macmini busy: 31.0 GB free, 14 threads 86% busy, 1 sessions
18:36:11
18:36:11 up 8 days 3:18, 0 users, load average: 0.79, 1.41, 1.33
ludothinkpad ok: 25.9 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 93% busy, 1 sessions
18:37:15
18:37:15 up 8 days 3:20, 0 users, load average: 1.44, 1.47, 1.36
ludothinkpad ok: 25.9 GB free, 4 threads 23% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 86% busy, 1 sessions
18:38:19
18:38:19 up 8 days 3:21, 0 users, load average: 1.37, 1.45, 1.36
ludothinkpad ok: 25.9 GB free, 4 threads 13% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 86% busy, 1 sessions
18:39:24
18:39:24 up 8 days 3:22, 0 users, load average: 2.27, 1.66, 1.44
ludothinkpad ok: 28.3 GB free, 4 threads 62% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 70% busy, 1 sessions
18:40:28
18:40:28 up 8 days 3:23, 0 users, load average: 1.95, 1.72, 1.48
ludothinkpad ok: 28.3 GB free, 4 threads 63% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 84% busy, 1 sessions
18:41:32
18:41:32 up 8 days 3:24, 0 users, load average: 3.32, 2.22, 1.67
ludothinkpad ok: 28.2 GB free, 4 threads 61% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 85% busy, 1 sessions
18:42:36
18:42:36 up 8 days 3:25, 0 users, load average: 3.13, 2.33, 1.75
ludothinkpad ok: 28.2 GB free, 4 threads 60% busy, 0 sessions
desktop offline
macmini busy: 27.1 GB free, 14 threads 91% busy, 1 sessions
18:43:40
18:43:40 up 8 days 3:26, 0 users, load average: 3.27, 2.48, 1.83
ludothinkpad busy: 28.1 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 92% busy, 1 sessions
18:44:44
18:44:44 up 8 days 3:27, 0 users, load average: 2.14, 2.32, 1.82
ludothinkpad ok: 28.1 GB free, 4 threads 55% busy, 0 sessions
desktop offline
macmini busy: 30.4 GB free, 14 threads 88% busy, 1 sessions
18:45:48
18:45:48 up 8 days 3:28, 0 users, load average: 4.94, 3.06, 2.11
ludothinkpad busy: 25.9 GB free, 4 threads 86% busy, 0 sessions
desktop offline
macmini ok: 28.7 GB free, 14 threads 84% busy, 1 sessions
18:46:53
18:46:53 up 8 days 3:29, 0 users, load average: 6.55, 3.92, 2.47
ludothinkpad busy: 25.4 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 29.3 GB free, 14 threads 81% busy, 1 sessions
18:47:57
18:47:57 up 8 days 3:30, 0 users, load average: 8.50, 4.96, 2.93
ludothinkpad busy: 24.0 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 27.9 GB free, 14 threads 90% busy, 1 sessions
18:49:01
18:49:01 up 8 days 3:31, 0 users, load average: 8.59, 5.68, 3.31
ludothinkpad busy: 22.7 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.2 GB free, 14 threads 85% busy, 1 sessions
18:50:05
18:50:05 up 8 days 3:32, 0 users, load average: 7.81, 6.07, 3.61
ludothinkpad busy: 24.5 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.6 GB free, 14 threads 96% busy, 1 sessions
18:51:09
18:51:09 up 8 days 3:33, 0 users, load average: 8.29, 6.50, 3.93
ludothinkpad busy: 24.3 GB free, 4 threads 92% busy, 0 sessions
desktop offline
macmini busy: 28.0 GB free, 14 threads 94% busy, 1 sessions
18:52:14
18:52:14 up 8 days 3:35, 0 users, load average: 5.58, 6.06, 3.95
ludothinkpad busy: 26.0 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 26.3 GB free, 14 threads 87% busy, 1 sessions
18:53:18
18:53:18 up 8 days 3:36, 0 users, load average: 4.53, 5.69, 3.97
ludothinkpad busy: 27.2 GB free, 4 threads 89% busy, 0 sessions
desktop offline
macmini busy: 26.8 GB free, 14 threads 87% busy, 1 sessions
18:54:22
18:54:22 up 8 days 3:37, 0 users, load average: 5.31, 5.64, 4.07
ludothinkpad busy: 26.3 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 29.3 GB free, 14 threads 91% busy, 1 sessions
18:55:26
18:55:26 up 8 days 3:38, 0 users, load average: 10.47, 6.99, 4.63
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 26.7 GB free, 14 threads 90% busy, 1 sessions
18:56:30
18:56:30 up 8 days 3:39, 0 users, load average: 9.27, 7.45, 4.96
ludothinkpad busy: 26.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.5 GB free, 14 threads 92% busy, 1 sessions
18:57:35
18:57:35 up 8 days 3:40, 0 users, load average: 7.79, 7.39, 5.10
ludothinkpad busy: 26.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 27.1 GB free, 14 threads 80% busy, 1 sessions
18:58:39
18:58:39 up 8 days 3:41, 0 users, load average: 8.06, 7.55, 5.32
ludothinkpad busy: 26.4 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.8 GB free, 14 threads 95% busy, 1 sessions
18:59:43
18:59:43 up 8 days 3:42, 0 users, load average: 8.44, 7.65, 5.50
ludothinkpad busy: 26.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.4 GB free, 14 threads 94% busy, 1 sessions
19:00:47
19:00:47 up 8 days 3:43, 0 users, load average: 11.04, 8.66, 6.00
ludothinkpad busy: 24.9 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 31.8 GB free, 14 threads 88% busy, 1 sessions
19:01:51
19:01:51 up 8 days 3:44, 0 users, load average: 11.91, 9.37, 6.41
ludothinkpad busy: 24.4 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 32.9 GB free, 14 threads 77% busy, 1 sessions
19:02:56
19:02:56 up 8 days 3:45, 0 users, load average: 11.64, 9.85, 6.79
ludothinkpad ok: 25.5 GB free, 4 threads 70% busy, 0 sessions
desktop offline
macmini busy: 30.9 GB free, 14 threads 91% busy, 1 sessions
19:04:00
19:04:00 up 8 days 3:46, 0 users, load average: 11.42, 10.06, 7.06
ludothinkpad busy: 23.4 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini busy: 28.4 GB free, 14 threads 94% busy, 1 sessions
19:05:04
19:05:04 up 8 days 3:47, 0 users, load average: 9.83, 9.96, 7.24
ludothinkpad busy: 23.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 31.4 GB free, 14 threads 80% busy, 1 sessions
19:06:09
19:06:09 up 8 days 3:48, 0 users, load average: 8.38, 9.68, 7.34
ludothinkpad ok: 22.7 GB free, 4 threads 17% busy, 0 sessions
desktop offline
macmini busy: 29.8 GB free, 14 threads 86% busy, 1 sessions
19:07:13
19:07:13 up 8 days 3:49, 0 users, load average: 4.51, 8.27, 7.00
ludothinkpad ok: 25.7 GB free, 4 threads 75% busy, 0 sessions
desktop offline
macmini busy: 26.5 GB free, 14 threads 89% busy, 1 sessions
19:08:17
19:08:17 up 8 days 3:51, 0 users, load average: 6.77, 8.13, 7.04
ludothinkpad busy: 25.8 GB free, 4 threads 95% busy, 0 sessions
desktop offline
macmini busy: 30.8 GB free, 14 threads 91% busy, 1 sessions
19:09:22
19:09:22 up 8 days 3:52, 0 users, load average: 8.27, 8.25, 7.14
ludothinkpad busy: 25.2 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.3 GB free, 14 threads 91% busy, 1 sessions
19:10:26
19:10:26 up 8 days 3:53, 0 users, load average: 8.88, 8.57, 7.34
ludothinkpad busy: 24.8 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 32.4 GB free, 14 threads 72% busy, 1 sessions
19:11:30
19:11:30 up 8 days 3:54, 0 users, load average: 13.33, 10.05, 7.94
ludothinkpad busy: 24.7 GB free, 4 threads 97% busy, 0 sessions
desktop offline
macmini busy: 30.1 GB free, 14 threads 87% busy, 1 sessions
19:12:35
19:12:35 up 8 days 3:55, 0 users, load average: 10.92, 10.13, 8.12
ludothinkpad ok: 26.4 GB free, 4 threads 76% busy, 0 sessions
desktop offline
macmini busy: 29.3 GB free, 14 threads 86% busy, 1 sessions
19:13:39
19:13:39 up 8 days 3:56, 0 users, load average: 9.98, 9.89, 8.16
ludothinkpad busy: 25.8 GB free, 4 threads 90% busy, 0 sessions
desktop offline
macmini busy: 28.8 GB free, 14 threads 93% busy, 1 sessions
19:14:43
19:14:43 up 8 days 3:57, 0 users, load average: 8.95, 9.63, 8.19
ludothinkpad busy: 25.1 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 29.6 GB free, 14 threads 71% busy, 1 sessions
19:15:47
19:15:47 up 8 days 3:58, 0 users, load average: 12.02, 10.38, 8.55
ludothinkpad busy: 24.5 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini busy: 28.1 GB free, 14 threads 93% busy, 1 sessions
19:16:52
19:16:52 up 8 days 3:59, 0 users, load average: 13.82, 11.21, 8.95
ludothinkpad busy: 23.9 GB free, 4 threads 100% busy, 0 sessions
desktop offline
macmini ok: 28.4 GB free, 14 threads 75% busy, 1 sessions
19:17:56
19:17:56 up 8 days 4:00, 0 users, load average: 11.95, 11.20, 9.10
ludothinkpad busy: 25.0 GB free, 4 threads 99% busy, 0 sessions
desktop offline
macmini ok: 28.4 GB free, 14 threads 81% busy, 1 sessions
19:19:00
19:19:00 up 8 days 4:01, 0 users, load average: 11.46, 11.14, 9.22
ludothinkpad busy: 24.6 GB free, 4 threads 95% busy, 0 sessions
desktop offline
macmini ok: 26.4 GB free, 14 threads 83% busy, 1 sessions
19:20:04
19:20:04 up 8 days 4:02, 0 users, load average: 11.11, 10.97, 9.28
ludothinkpad busy: 24.6 GB free, 4 threads 93% busy, 0 sessions
desktop offline
macmini ok: 26.7 GB free, 14 threads 85% busy, 1 sessions
19:21:08
19:21:08 up 8 days 4:03, 0 users, load average: 9.81, 10.60, 9.27
ludothinkpad busy: 24.4 GB free, 4 threads 91% busy, 0 sessions
desktop offline
macmini ok: 27.8 GB free, 14 threads 75% busy, 1 sessions
19:22:13
19:22:13 up 8 days 4:04, 0 users, load average: 5.98, 9.51, 9.00
ludothinkpad ok: 23.8 GB free, 4 threads 18% busy, 0 sessions
desktop offline
macmini busy: 29.7 GB free, 14 threads 85% busy, 1 sessions
19:23:17
19:23:17 up 8 days 4:06, 0 users, load average: 4.01, 8.27, 8.60
ludothinkpad ok: 23.1 GB free, 4 threads 56% busy, 0 sessions
desktop offline
macmini ok: 28.2 GB free, 14 threads 79% busy, 1 sessions
@@ -1,367 +0,0 @@
{
"kind": "horizon-server-benchmark",
"date": "2026-10-18T17:23:30+00:00",
"machine": {
"system": "Linux",
"machine": "x86_64",
"cpus": 4,
"note": "ThinkPad: Intel Core i7-7500U (2 cores, 4 threads), 32 GB, NixOS, Linux 6.18; the bots on the Mac mini (M4 Pro) over the tailnet"
},
"commit": "4ea2243e4d1a44f910857bd759845db70e3690a8",
"java": [
"openjdk 25.0.4.1 2026-08-18"
],
"settings": {
"servers": "horizon,vanilla",
"bots": "1,10,25,50",
"behaviour": "mixed",
"join_every": 1.0,
"warmup": 60,
"seconds": 300,
"seed": 12345,
"explore_speed": 0,
"gamerule": "",
"heap": "4g",
"out": "mixed.json",
"nice": 19,
"bots_via": "ssh macmini",
"bots_bin": "horizon-bots (on the Mac mini)",
"connect": "<the ThinkPad's tailnet address>",
"properties": {
"level-seed": "12345",
"view-distance": "10",
"simulation-distance": "10",
"online-mode": "false",
"white-list": "false",
"gamemode": "creative",
"force-gamemode": "true",
"spawn-protection": "0",
"max-players": "200",
"difficulty": "easy",
"sync-chunk-writes": "true"
}
},
"runs": [
{
"server": "horizon",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 10.92,
"mspt": {
"average": 2.38,
"p50": 1.97,
"p95": 5.17,
"p99": 18.2
},
"mspt_worst_p99": 45.6,
"tps": 20.0,
"rss_mb": {
"mean": 653.7,
"max": 659.2
},
"cpu_percent": 24.51,
"chunks_per_second": 9.9,
"unique_per_second": 0.3,
"forgotten_per_second": 9.93,
"waited_ticks_per_second": 0.0,
"chunks_total": 3951,
"chunks_per_bot": {
"walk": 3951.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 1,
"behaviour": "mixed",
"start_seconds": 14.75,
"mspt": {
"average": 17.9,
"p50": 18.39,
"p95": 27.58,
"p99": 47.28
},
"mspt_worst_p99": 423.8,
"tps": 20.0,
"rss_mb": {
"mean": 3382.06,
"max": 3394.1
},
"cpu_percent": 64.02,
"chunks_per_second": 9.92,
"unique_per_second": 0.29,
"forgotten_per_second": 9.95,
"waited_ticks_per_second": 0.0,
"chunks_total": 3846,
"chunks_per_bot": {
"walk": 3846.0
},
"unique_chunks_per_bot": {
"walk": 699.0
},
"resent": 0,
"pulled_back": 0,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 1,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 10.22,
"mspt": {
"average": 12.39,
"p50": 8.33,
"p95": 35.99,
"p99": 65.9
},
"mspt_worst_p99": 117.1,
"tps": 20.0,
"rss_mb": {
"mean": 935.85,
"max": 1008.6
},
"cpu_percent": 210.37,
"chunks_per_second": 98.22,
"unique_per_second": 36.49,
"forgotten_per_second": 98.25,
"waited_ticks_per_second": 0.0,
"chunks_total": 39618,
"chunks_per_bot": {
"walk": 3992.17,
"explore": 7359.5,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 696.83,
"explore": 6869.0,
"idle": 473.0
},
"resent": 0,
"pulled_back": 6267,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 10,
"behaviour": "mixed",
"start_seconds": 14.54,
"mspt": {
"average": 34.47,
"p50": 29.67,
"p95": 63.52,
"p99": 138.58
},
"mspt_worst_p99": 884.7,
"tps": 20.0,
"rss_mb": {
"mean": 3594.26,
"max": 3661.7
},
"cpu_percent": 217.13,
"chunks_per_second": 24.34,
"unique_per_second": 19.13,
"forgotten_per_second": 20.12,
"waited_ticks_per_second": 0.62,
"chunks_total": 10443,
"chunks_per_bot": {
"walk": 679.83,
"explore": 2824.0,
"idle": 358.0
},
"unique_chunks_per_bot": {
"walk": 389.17,
"explore": 2824.0,
"idle": 358.0
},
"resent": 0,
"pulled_back": 5500,
"moved_too_quickly": 12,
"moved_wrongly": 0,
"bots_joined": 10,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "horizon",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 14.73,
"mspt": {
"average": 24.0,
"p50": 21.07,
"p95": 49.72,
"p99": 80.57
},
"mspt_worst_p99": 168.8,
"tps": 20.0,
"rss_mb": {
"mean": 1013.07,
"max": 1179.4
},
"cpu_percent": 192.47,
"chunks_per_second": 178.23,
"unique_per_second": 31.85,
"forgotten_per_second": 177.93,
"waited_ticks_per_second": 60.59,
"chunks_total": 73657,
"chunks_per_bot": {
"walk": 3927.87,
"explore": 2474.8,
"idle": 473.0
},
"unique_chunks_per_bot": {
"walk": 691.73,
"explore": 2447.8,
"idle": 473.0
},
"resent": 0,
"pulled_back": 5532,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 58
},
{
"server": "vanilla",
"bots": 25,
"behaviour": "mixed",
"start_seconds": 28.57,
"mspt": {
"average": 31.59,
"p50": 25.18,
"p95": 70.03,
"p99": 222.74
},
"mspt_worst_p99": 2319.6,
"tps": 20.0,
"rss_mb": {
"mean": 3742.51,
"max": 4055.0
},
"cpu_percent": 218.34,
"chunks_per_second": 30.37,
"unique_per_second": 10.84,
"forgotten_per_second": 29.44,
"waited_ticks_per_second": 50.91,
"chunks_total": 14193,
"chunks_per_bot": {
"walk": 640.2,
"explore": 738.0,
"idle": 180.0
},
"unique_chunks_per_bot": {
"walk": 211.33,
"explore": 737.6,
"idle": 180.0
},
"resent": 0,
"pulled_back": 4524,
"moved_too_quickly": 324,
"moved_wrongly": 0,
"bots_joined": 25,
"bots_failed": [],
"tick_queries": 56
},
{
"server": "horizon",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 19.84,
"mspt": {
"average": 35.2,
"p50": 32.12,
"p95": 69.04,
"p99": 96.56
},
"mspt_worst_p99": 240.8,
"tps": 20.0,
"rss_mb": {
"mean": 1013.2,
"max": 1249.1
},
"cpu_percent": 217.68,
"chunks_per_second": 273.18,
"unique_per_second": 34.58,
"forgotten_per_second": 268.19,
"waited_ticks_per_second": 138.29,
"chunks_total": 115527,
"chunks_per_bot": {
"walk": 3300.1,
"explore": 1192.4,
"idle": 460.0
},
"unique_chunks_per_bot": {
"walk": 632.67,
"explore": 1186.9,
"idle": 460.0
},
"resent": 0,
"pulled_back": 8369,
"moved_too_quickly": 0,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 57
},
{
"server": "vanilla",
"bots": 50,
"behaviour": "mixed",
"start_seconds": 20.58,
"mspt": {
"average": 29.74,
"p50": 23.6,
"p95": 62.25,
"p99": 137.41
},
"mspt_worst_p99": 489.3,
"tps": 20.0,
"rss_mb": {
"mean": 3995.71,
"max": 4597.8
},
"cpu_percent": 294.31,
"chunks_per_second": 94.36,
"unique_per_second": 17.76,
"forgotten_per_second": 94.69,
"waited_ticks_per_second": 101.48,
"chunks_total": 41637,
"chunks_per_bot": {
"walk": 1104.97,
"explore": 628.8,
"idle": 220.0
},
"unique_chunks_per_bot": {
"walk": 279.4,
"explore": 626.4,
"idle": 220.0
},
"resent": 0,
"pulled_back": 12034,
"moved_too_quickly": 2193,
"moved_wrongly": 0,
"bots_joined": 50,
"bots_failed": [],
"tick_queries": 57
}
],
"horizon_build": "v2026.10.18.15 release binaries (horizon-client, the engine, at 6c078b28); horizon-server from #898 (4ea2243e, the supervisor only)"
}
@@ -1,379 +0,0 @@
{
"scope": "Assets + BlockFacts + Behaviours loader only; no full startup or first frame",
"resources": "/srv/bob-data/horizon/.local/horizon-client/26.3",
"repeats": 10,
"ordering": [
[
"released",
"retained"
],
[
"retained",
"released"
],
[
"released",
"retained"
],
[
"retained",
"released"
],
[
"released",
"retained"
],
[
"retained",
"released"
],
[
"released",
"retained"
],
[
"retained",
"released"
],
[
"released",
"retained"
],
[
"retained",
"released"
]
],
"load_average_before": [
8.2421875,
6.56396484375,
6.39208984375
],
"load_average_after": [
7.98095703125,
6.6455078125,
6.4248046875
],
"executables": {
"released": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-integrated",
"mode": "deferred",
"sha256": "a52d6a7efc7b3198ef0dd8150325f56a3fe5d0c5883fd6d14c4b0c281866ce7c"
},
"retained": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-integrated",
"mode": "deferred-held",
"sha256": "a52d6a7efc7b3198ef0dd8150325f56a3fe5d0c5883fd6d14c4b0c281866ce7c"
}
},
"metrics": {
"released": {
"phase_ms": {
"median": 482.591,
"p25": 444.276,
"p75": 586.92,
"min": 430.128,
"max": 736.543,
"samples": [
529.244,
606.146,
449.23,
442.624,
669.108,
511.519,
736.543,
432.616,
430.128,
453.663
]
},
"total_ms": {
"median": 987.004,
"p25": 932.885,
"p75": 1134.876,
"min": 914.242,
"max": 1201.759,
"samples": [
1055.119,
1161.462,
968.629,
914.242,
1201.759,
1005.379,
1200.01,
928.249,
935.775,
931.921
]
},
"cpu_ms": {
"median": 1004.675,
"p25": 936.465,
"p75": 1116.791,
"min": 908.327,
"max": 1241.168,
"samples": [
1074.425,
1130.913,
984.832,
931.205,
1186.169,
1024.519,
1241.168,
908.327,
952.245,
927.15
]
},
"peak_rss_kib": {
"median": 268124.0,
"p25": 267753.0,
"p75": 268455.0,
"min": 267708,
"max": 268700,
"samples": [
268268,
268500,
267744,
267724,
267708,
267780,
268700,
268496,
268332,
267980
]
},
"behaviour_json_ms": {
"median": 24.371,
"p25": 22.469,
"p75": 28.486,
"min": 20.662,
"max": 35.297,
"samples": [
33.387,
35.297,
24.732,
24.011,
27.699,
28.748,
23.623,
22.084,
20.662,
21.266
]
},
"facts_ms": {
"median": 292.77,
"p25": 270.649,
"p75": 383.193,
"min": 264.691,
"max": 443.291,
"samples": [
346.613,
398.468,
274.673,
264.691,
443.291,
292.926,
395.387,
267.232,
269.308,
292.615
]
},
"behaviours_ms": {
"median": 148.635,
"p25": 141.329,
"p75": 181.741,
"min": 136.361,
"max": 307.004,
"samples": [
145.009,
168.982,
146.535,
150.736,
194.604,
185.994,
307.004,
140.102,
136.785,
136.361
]
},
"post_rss_kib": {
"median": 268668.0,
"p25": 268246.0,
"p75": 268922.0,
"min": 268048.0,
"max": 269012.0,
"samples": [
268668.0,
268936.0,
268108.0,
268088.0,
268048.0,
268668.0,
268880.0,
269012.0,
268960.0,
268660.0
]
}
},
"retained": {
"phase_ms": {
"median": 487.954,
"p25": 471.456,
"p75": 531.586,
"min": 401.516,
"max": 834.154,
"samples": [
486.974,
613.755,
540.535,
470.333,
488.934,
474.825,
834.154,
504.74,
401.516,
440.381
]
},
"total_ms": {
"median": 1038.875,
"p25": 989.724,
"p75": 1079.197,
"min": 880.933,
"max": 1746.386,
"samples": [
996.407,
1067.467,
1082.69,
1103.131,
1010.283,
1068.719,
1746.386,
987.496,
890.46,
880.933
]
},
"cpu_ms": {
"median": 1035.537,
"p25": 982.276,
"p75": 1082.698,
"min": 908.774,
"max": 1660.068,
"samples": [
1011.712,
1087.688,
1076.523,
1084.757,
998.654,
1059.363,
1660.068,
976.817,
912.849,
908.774
]
},
"peak_rss_kib": {
"median": 268150.0,
"p25": 267537.0,
"p75": 268614.0,
"min": 267268,
"max": 268848,
"samples": [
268748,
267968,
267268,
268332,
267540,
268560,
267492,
268632,
267536,
268848
]
},
"behaviour_json_ms": {
"median": 23.208,
"p25": 22.305,
"p75": 26.544,
"min": 20.059,
"max": 32.669,
"samples": [
32.669,
22.62,
22.2,
27.331,
25.088,
27.029,
23.488,
20.977,
22.928,
20.059
]
},
"facts_ms": {
"median": 304.44,
"p25": 291.562,
"p75": 330.991,
"min": 244.758,
"max": 384.634,
"samples": [
307.936,
374.145,
336.931,
300.944,
290.81,
293.817,
384.634,
313.171,
244.758,
279.303
]
},
"behaviours_ms": {
"median": 162.285,
"p25": 143.135,
"p75": 179.31,
"min": 133.83,
"max": 426.031,
"samples": [
146.367,
216.988,
181.402,
142.058,
173.036,
153.978,
426.031,
170.592,
133.83,
141.017
]
},
"post_rss_kib": {
"median": 268694.0,
"p25": 268116.0,
"p75": 269056.0,
"min": 267988.0,
"max": 269212.0,
"samples": [
269212.0,
268564.0,
268096.0,
268824.0,
268176.0,
269032.0,
267988.0,
269064.0,
268044.0,
269120.0
]
}
}
}
}
@@ -1,176 +0,0 @@
{
"scope": "Isolated gameplay-facts.json validation/typed decoding; includes JSON file read",
"resources": "/srv/bob-data/horizon/.local/horizon-client/26.3",
"repeats": 10,
"load_averages_before_after": [
[
14.7451171875,
8.8994140625,
6.89599609375
],
[
14.7451171875,
8.8994140625,
6.89599609375
]
],
"binary_sha256": "c2be9140c792720499d70c5806b8def0472734264ad72486d01365447e0cc2d7",
"metrics": {
"validate": {
"phase_ms": {
"median": 117.417,
"p25": 114.101,
"p75": 158.855,
"min": 109.199,
"max": 173.408,
"samples": [
109.199,
113.958,
110.195,
118.987,
171.275,
124.701,
170.24,
173.408,
115.848,
114.531
]
},
"total_ms": {
"median": 139.933,
"p25": 133.863,
"p75": 193.565,
"min": 129.75,
"max": 276.311,
"samples": [
276.311,
135.448,
129.75,
138.452,
195.127,
141.413,
188.88,
199.802,
133.335,
132.094
]
},
"cpu_ms": {
"median": 138.849,
"p25": 134.285,
"p75": 168.881,
"min": 129.089,
"max": 190.82,
"samples": [
140.962,
136.06,
129.089,
139.059,
189.478,
138.639,
178.187,
190.82,
133.693,
132.716
]
},
"peak_rss_kib": {
"median": 56654.0,
"p25": 56642.0,
"p75": 56704.0,
"min": 56616,
"max": 56724,
"samples": [
56620,
56656,
56640,
56720,
56616,
56724,
56652,
56720,
56648,
56656
]
}
},
"typed": {
"phase_ms": {
"median": 115.873,
"p25": 109.422,
"p75": 129.67,
"min": 100.872,
"max": 183.923,
"samples": [
116.965,
132.071,
100.872,
105.098,
122.468,
170.777,
107.997,
183.923,
113.699,
114.781
]
},
"total_ms": {
"median": 133.883,
"p25": 126.939,
"p75": 155.913,
"min": 117.54,
"max": 210.164,
"samples": [
135.04,
159.38,
117.54,
122.391,
145.512,
189.132,
125.846,
210.164,
130.217,
132.727
]
},
"cpu_ms": {
"median": 134.375,
"p25": 126.231,
"p75": 154.675,
"min": 118.132,
"max": 204.242,
"samples": [
135.633,
157.961,
118.132,
122.77,
144.816,
178.293,
124.755,
204.242,
130.661,
133.117
]
},
"peak_rss_kib": {
"median": 83974.0,
"p25": 82744.0,
"p75": 84490.0,
"min": 82332,
"max": 84596,
"samples": [
83596,
84596,
82332,
82424,
84288,
82460,
84532,
83660,
84364,
84536
]
}
}
}
}
@@ -1,453 +0,0 @@
{
"scope": "Assets + BlockFacts + Behaviours loader only; no full startup or first frame",
"resources": "/srv/bob-data/horizon/.local/horizon-client/26.3",
"repeats": 8,
"ordering": [
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
]
],
"load_average_before": [
10.57958984375,
8.45947265625,
6.8134765625
],
"load_average_after": [
7.158203125,
7.7998046875,
6.66455078125
],
"executables": {
"baseline": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-e422bd13",
"mode": "baseline",
"sha256": "bf61f2e6c92e001be3c3bc93cf8072cc6af6fd1d7bcfa7273045960948ec89e7"
},
"a01d47f2": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-a01d47f2",
"mode": "shared",
"sha256": "88be56f8dbd28d0b4abd6c672486ba758df96ba18f68da45b94db14231dd3eea"
},
"cf837118": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-cf837118",
"mode": "deferred",
"sha256": "f1b044cd9ac2f67c644fb40bd7ecbe38128ec739a8112d57fc36f05fe9036a52"
},
"cd61e11b": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-cd61e11b",
"mode": "deferred",
"sha256": "fb2d3725b09ee38f85b49901f7053a6fe339b980a2b2dac6930a8ded22c139e8"
},
"current": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-new",
"mode": "deferred",
"sha256": "84eb1baae8e138f52e300b0b44f1339fca38e70db6e06126e78db776ea3e5cd0"
}
},
"metrics": {
"baseline": {
"phase_ms": {
"median": 1277.781,
"p25": 1271.23,
"p75": 1296.157,
"min": 1223.341,
"max": 1338.745,
"samples": [
1265.934,
1223.341,
1274.931,
1289.177,
1338.745,
1317.096,
1272.995,
1280.631
]
},
"total_ms": {
"median": 1711.802,
"p25": 1695.745,
"p75": 1754.276,
"min": 1645.69,
"max": 2887.078,
"samples": [
2887.078,
1645.69,
1690.413,
1711.459,
1761.859,
1751.748,
1697.522,
1712.144
]
},
"cpu_ms": {
"median": 1769.996,
"p25": 1748.925,
"p75": 1807.254,
"min": 1705.167,
"max": 1950.463,
"samples": [
1950.463,
1705.167,
1749.023,
1768.05,
1804.543,
1815.386,
1771.942,
1748.632
]
},
"peak_rss_kib": {
"median": 820180.0,
"p25": 820030.0,
"p75": 820502.0,
"min": 819472,
"max": 820884,
"samples": [
819472,
820416,
820060,
820096,
820264,
820884,
819940,
820760
]
}
},
"a01d47f2": {
"phase_ms": {
"median": 430.783,
"p25": 427.542,
"p75": 451.763,
"min": 423.035,
"max": 550.345,
"samples": [
491.599,
423.035,
428.85,
427.694,
432.715,
427.084,
438.484,
550.345
]
},
"total_ms": {
"median": 868.457,
"p25": 863.709,
"p75": 932.066,
"min": 837.75,
"max": 997.779,
"samples": [
911.935,
837.75,
865.419,
868.626,
858.578,
997.779,
868.288,
992.46
]
},
"cpu_ms": {
"median": 897.639,
"p25": 887.661,
"p75": 944.363,
"min": 865.497,
"max": 1001.635,
"samples": [
925.321,
865.497,
899.505,
895.774,
876.929,
1001.635,
891.239,
1001.488
]
},
"peak_rss_kib": {
"median": 351906.0,
"p25": 351505.0,
"p75": 352181.0,
"min": 351176,
"max": 352440,
"samples": [
352008,
352120,
351352,
351556,
351804,
351176,
352440,
352364
]
}
},
"cf837118": {
"phase_ms": {
"median": 408.459,
"p25": 398.987,
"p75": 428.585,
"min": 387.999,
"max": 524.425,
"samples": [
433.521,
387.999,
412.195,
397.366,
404.724,
399.527,
426.939,
524.425
]
},
"total_ms": {
"median": 851.294,
"p25": 823.291,
"p75": 890.55,
"min": 801.356,
"max": 1030.737,
"samples": [
892.054,
801.356,
840.381,
818.285,
862.208,
824.96,
890.049,
1030.737
]
},
"cpu_ms": {
"median": 870.765,
"p25": 850.849,
"p75": 907.922,
"min": 825.718,
"max": 1041.996,
"samples": [
906.657,
825.718,
866.472,
843.591,
875.058,
853.268,
911.715,
1041.996
]
},
"peak_rss_kib": {
"median": 274172.0,
"p25": 273909.0,
"p75": 274438.0,
"min": 273668,
"max": 274964,
"samples": [
274372,
274964,
273668,
273972,
274552,
274400,
273840,
273932
]
}
},
"cd61e11b": {
"phase_ms": {
"median": 419.114,
"p25": 404.191,
"p75": 460.634,
"min": 388.557,
"max": 735.687,
"samples": [
457.971,
418.303,
388.557,
405.041,
419.925,
401.64,
468.624,
735.687
]
},
"total_ms": {
"median": 840.723,
"p25": 835.398,
"p75": 917.995,
"min": 811.43,
"max": 1565.471,
"samples": [
906.328,
837.706,
811.43,
835.727,
843.74,
834.412,
952.996,
1565.471
]
},
"cpu_ms": {
"median": 863.404,
"p25": 858.916,
"p75": 934.2,
"min": 833.955,
"max": 1550.132,
"samples": [
916.017,
864.82,
833.955,
859.057,
861.989,
858.494,
988.748,
1550.132
]
},
"peak_rss_kib": {
"median": 267508.0,
"p25": 267310.0,
"p75": 267890.0,
"min": 267172,
"max": 268412,
"samples": [
267304,
268292,
267312,
267756,
267340,
267676,
267172,
268412
]
}
},
"current": {
"phase_ms": {
"median": 406.779,
"p25": 397.839,
"p75": 413.957,
"min": 385.028,
"max": 552.051,
"samples": [
387.136,
385.028,
409.317,
404.242,
401.407,
411.451,
421.476,
552.051
]
},
"total_ms": {
"median": 819.787,
"p25": 814.211,
"p75": 855.69,
"min": 800.867,
"max": 1128.453,
"samples": [
808.685,
800.867,
816.053,
817.907,
821.667,
843.26,
892.982,
1128.453
]
},
"cpu_ms": {
"median": 847.078,
"p25": 836.598,
"p75": 877.524,
"min": 826.002,
"max": 1134.369,
"samples": [
834.665,
826.002,
837.243,
842.86,
851.296,
864.892,
915.419,
1134.369
]
},
"peak_rss_kib": {
"median": 267956.0,
"p25": 267612.0,
"p75": 268326.0,
"min": 267308,
"max": 268428,
"samples": [
268428,
267308,
268260,
267516,
267652,
268312,
268368,
267644
]
}
}
},
"experiment_note": "Experimental projection variant (reused sounds, Arc-keyed string pools and direct flags); reverted after mixed CPU/RSS evidence. Current in this file is not the final PR version."
}
@@ -1,582 +0,0 @@
{
"scope": "Assets + BlockFacts + Behaviours loader only; no full startup or first frame",
"resources": "/srv/bob-data/horizon/.local/horizon-client/26.3",
"repeats": 10,
"ordering": [
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
],
[
"baseline",
"a01d47f2",
"cf837118",
"cd61e11b",
"current"
],
[
"current",
"cd61e11b",
"cf837118",
"a01d47f2",
"baseline"
]
],
"load_average_before": [
11.4921875,
7.46875,
6.69677734375
],
"load_average_after": [
17.763671875,
11.39013671875,
8.20556640625
],
"executables": {
"baseline": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-e422bd13",
"mode": "baseline",
"sha256": "bf61f2e6c92e001be3c3bc93cf8072cc6af6fd1d7bcfa7273045960948ec89e7"
},
"a01d47f2": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-a01d47f2",
"mode": "shared",
"sha256": "88be56f8dbd28d0b4abd6c672486ba758df96ba18f68da45b94db14231dd3eea"
},
"cf837118": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-cf837118",
"mode": "deferred",
"sha256": "f1b044cd9ac2f67c644fb40bd7ecbe38128ec739a8112d57fc36f05fe9036a52"
},
"cd61e11b": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-cd61e11b",
"mode": "deferred",
"sha256": "fb2d3725b09ee38f85b49901f7053a6fe339b980a2b2dac6930a8ded22c139e8"
},
"current": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-integrated",
"mode": "deferred",
"sha256": "a52d6a7efc7b3198ef0dd8150325f56a3fe5d0c5883fd6d14c4b0c281866ce7c"
}
},
"metrics": {
"baseline": {
"phase_ms": {
"median": 1506.588,
"p25": 1388.314,
"p75": 2119.85,
"min": 1319.319,
"max": 3143.124,
"samples": [
3143.124,
2483.307,
2124.952,
2104.544,
1525.153,
1396.75,
1381.771,
1488.022,
1385.502,
1319.319
]
},
"total_ms": {
"median": 1962.122,
"p25": 1858.188,
"p75": 3152.608,
"min": 1804.713,
"max": 4136.912,
"samples": [
4136.912,
3250.285,
3134.13,
3158.767,
2001.229,
1883.076,
1849.892,
1923.016,
1818.34,
1804.713
]
},
"cpu_ms": {
"median": 1969.296,
"p25": 1893.348,
"p75": 2694.824,
"min": 1842.049,
"max": 3059.824,
"samples": [
3059.824,
2765.392,
2698.837,
2682.786,
2026.89,
1911.703,
1888.282,
1908.545,
1859.233,
1842.049
]
},
"peak_rss_kib": {
"median": 819952.0,
"p25": 819778.0,
"p75": 820164.0,
"min": 818944,
"max": 820304,
"samples": [
819956,
819744,
818944,
818984,
820128,
820176,
819948,
820236,
819880,
820304
]
}
},
"a01d47f2": {
"phase_ms": {
"median": 666.765,
"p25": 520.278,
"p75": 1156.022,
"min": 450.677,
"max": 1882.249,
"samples": [
1001.208,
1309.911,
1207.627,
1882.249,
747.233,
544.933,
512.06,
450.677,
466.596,
586.298
]
},
"total_ms": {
"median": 1141.968,
"p25": 974.723,
"p75": 2326.553,
"min": 922.496,
"max": 3158.114,
"samples": [
2374.699,
2275.18,
2343.677,
3158.114,
1267.594,
1006.411,
964.16,
922.496,
936.687,
1016.342
]
},
"cpu_ms": {
"median": 1106.901,
"p25": 973.629,
"p75": 1525.998,
"min": 929.573,
"max": 1568.573,
"samples": [
1545.278,
1568.573,
1468.158,
1563.377,
1196.236,
1017.565,
960.219,
929.573,
946.096,
1013.861
]
},
"peak_rss_kib": {
"median": 351564.0,
"p25": 351148.0,
"p75": 352106.0,
"min": 350416,
"max": 353368,
"samples": [
351572,
351268,
351108,
350416,
351656,
353368,
352256,
351556,
350964,
352308
]
}
},
"cf837118": {
"phase_ms": {
"median": 505.124,
"p25": 435.315,
"p75": 1103.87,
"min": 417.056,
"max": 1875.147,
"samples": [
1875.147,
1129.722,
1066.305,
1116.391,
461.019,
423.38,
417.056,
445.353,
549.229,
431.969
]
},
"total_ms": {
"median": 1209.375,
"p25": 895.862,
"p75": 2133.144,
"min": 872.434,
"max": 2936.234,
"samples": [
2936.234,
2215.538,
1885.964,
2695.131,
1378.495,
879.351,
897.968,
895.16,
1040.255,
872.434
]
},
"cpu_ms": {
"median": 1068.41,
"p25": 896.926,
"p75": 1488.578,
"min": 882.398,
"max": 1570.652,
"samples": [
1530.601,
1520.851,
1391.76,
1570.652,
1116.403,
900.779,
894.039,
895.642,
1020.417,
882.398
]
},
"peak_rss_kib": {
"median": 273832.0,
"p25": 273592.0,
"p75": 274131.0,
"min": 272732,
"max": 274976,
"samples": [
274080,
272732,
273560,
273468,
274200,
274148,
273728,
273936,
274976,
273688
]
}
},
"cd61e11b": {
"phase_ms": {
"median": 429.481,
"p25": 410.043,
"p75": 1178.091,
"min": 399.576,
"max": 1644.881,
"samples": [
1194.624,
1128.491,
1644.881,
1428.78,
405.544,
434.62,
424.341,
414.829,
408.447,
399.576
]
},
"total_ms": {
"median": 902.865,
"p25": 868.889,
"p75": 2443.447,
"min": 827.16,
"max": 2961.261,
"samples": [
2682.056,
2199.278,
2524.837,
2961.261,
884.838,
885.282,
848.79,
863.573,
920.448,
827.16
]
},
"cpu_ms": {
"median": 920.917,
"p25": 869.777,
"p75": 1475.629,
"min": 840.036,
"max": 1626.459,
"samples": [
1518.201,
1496.456,
1413.149,
1626.459,
893.762,
899.049,
861.782,
861.417,
942.785,
840.036
]
},
"peak_rss_kib": {
"median": 267762.0,
"p25": 267612.0,
"p75": 267966.0,
"min": 266300,
"max": 268636,
"samples": [
266672,
268636,
266300,
267596,
267660,
268280,
267968,
267680,
267960,
267844
]
}
},
"current": {
"phase_ms": {
"median": 460.489,
"p25": 429.004,
"p75": 986.935,
"min": 404.103,
"max": 1300.033,
"samples": [
992.891,
1126.098,
1300.033,
969.067,
443.604,
408.397,
472.429,
424.137,
448.549,
404.103
]
},
"total_ms": {
"median": 935.623,
"p25": 874.18,
"p75": 2282.679,
"min": 839.642,
"max": 2965.198,
"samples": [
2320.566,
2169.019,
2965.198,
2531.591,
924.347,
839.642,
946.899,
873.617,
875.87,
854.328
]
},
"cpu_ms": {
"median": 950.702,
"p25": 892.992,
"p75": 1540.918,
"min": 861.669,
"max": 1586.478,
"samples": [
1557.765,
1503.564,
1553.369,
1586.478,
931.391,
861.669,
970.013,
888.14,
907.549,
868.73
]
},
"peak_rss_kib": {
"median": 268010.0,
"p25": 267013.0,
"p75": 268385.0,
"min": 266144,
"max": 268876,
"samples": [
266824,
267580,
266516,
266144,
267976,
268304,
268044,
268480,
268412,
268876
]
},
"behaviour_json_ms": {
"median": 34.288,
"p25": 23.085,
"p75": 37.474,
"min": 20.137,
"max": 70.397,
"samples": [
37.07,
70.397,
37.609,
38.225,
34.568,
21.094,
23.469,
22.957,
34.008,
20.137
]
},
"facts_ms": {
"median": 272.839,
"p25": 256.553,
"p75": 618.472,
"min": 248.146,
"max": 780.388,
"samples": [
625.908,
693.735,
780.388,
596.165,
272.718,
249.954,
272.959,
264.913,
253.767,
248.146
]
},
"behaviours_ms": {
"median": 162.633,
"p25": 133.345,
"p75": 326.117,
"min": 132.828,
"max": 476.9,
"samples": [
325.198,
356.28,
476.9,
326.424,
132.828,
133.946,
167.929,
133.144,
157.337,
132.934
]
},
"post_rss_kib": {
"median": 268408.0,
"p25": 267369.0,
"p75": 268861.0,
"min": 267052.0,
"max": 269072.0,
"samples": [
267084.0,
268056.0,
267052.0,
267140.0,
268184.0,
268632.0,
268660.0,
269072.0,
268928.0,
268960.0
]
}
}
}
}
@@ -1,188 +0,0 @@
{
"scope": "Assets + block facts + behaviours loader only; excludes full startup",
"resources": ".local/horizon-client/26.3",
"repeats": 5,
"comparison": "historical cf837118 release binary",
"reference_binary": "target/release/examples/shared_json_startup_profile-cf837118",
"metrics": {
"baseline": {
"phase_ms": {
"median": 1462.003,
"samples": [
1639.999,
1506.864,
1429.844,
1462.003,
1346.061
]
},
"total_ms": {
"median": 1922.795,
"samples": [
3383.516,
1990.554,
1912.354,
1922.795,
1777.912
]
},
"cpu_ms": {
"median": 1961.459,
"samples": [
2419.086,
2021.457,
1951.587,
1961.459,
1839.984
]
},
"peak_rss_kib": {
"median": 820912,
"samples": [
819956,
821052,
820912,
821792,
820424
]
}
},
"pr607": {
"phase_ms": {
"median": 486.715,
"samples": [
460.738,
442.013,
623.053,
486.715,
550.652
]
},
"total_ms": {
"median": 948.089,
"samples": [
923.057,
935.599,
1109.64,
948.089,
1052.546
]
},
"cpu_ms": {
"median": 968.424,
"samples": [
928.489,
946.932,
1128.431,
968.424,
1079.642
]
},
"peak_rss_kib": {
"median": 351796,
"samples": [
351796,
351844,
351536,
351532,
351988
]
}
},
"cf837118": {
"phase_ms": {
"median": 474.268,
"samples": [
443.122,
474.268,
520.497,
433.525,
532.058
]
},
"total_ms": {
"median": 945.87,
"samples": [
934.722,
945.87,
1006.539,
888.201,
1032.6
]
},
"cpu_ms": {
"median": 953.668,
"samples": [
941.957,
953.668,
1029.122,
903.574,
1065.025
]
},
"peak_rss_kib": {
"median": 274476,
"samples": [
273448,
274844,
274476,
274556,
274252
]
}
},
"current": {
"phase_ms": {
"median": 445.924,
"samples": [
472.165,
427.797,
419.288,
445.924,
524.211
]
},
"total_ms": {
"median": 999.669,
"samples": [
999.669,
892.616,
1051.427,
940.595,
1112.158
]
},
"cpu_ms": {
"median": 1001.24,
"samples": [
1001.24,
887.874,
1051.732,
951.495,
1132.208
]
},
"peak_rss_kib": {
"median": 267604,
"samples": [
267716,
267920,
267448,
267604,
267344
]
}
}
},
"builds": {
"current": "release example built at cf837118 + current uncommitted delta in PR #607 worktree",
"historical": "release example built at exact cf837118 commit, with same Cargo release profile and shared dependency target"
},
"mode_commits": {
"baseline": "independent loader algorithm at e422bd13, executed in current binary",
"pr607": "shared early construction algorithm at a01d47f2, executed in current binary",
"cf837118": "exact historical binary at cf837118, deferred mode",
"current": "current PR worktree, deferred mode with interned strings"
},
"limitations": "Loader-only (Assets + BlockFacts + Behaviours), no full client startup or first-frame timing. Same release profile but different compilation contexts for historical binary; hardware activity and cache warmth vary. CPU and elapsed differences between cf837118 and current are mixed and may be noise. Peak RSS is whole-process Linux ru_maxrss."
}
@@ -1,93 +0,0 @@
{
"scope": "Assets + block facts + behaviours loader only; excludes full startup",
"resources": ".local/horizon-client/26.3",
"repeats": 5,
"metrics": {
"baseline": {
"phase_ms": {
"median": 4730.402,
"samples": [
3258.541,
6539.764,
4773.735,
3980.032,
4730.402
]
},
"total_ms": {
"median": 6325.026,
"samples": [
6596.141,
7627.893,
6296.443,
5435.408,
6325.026
]
},
"cpu_ms": {
"median": 3199.722,
"samples": [
3322.801,
3199.722,
3227.438,
3042.687,
3144.658
]
},
"peak_rss_kib": {
"median": 820000,
"samples": [
818540,
820400,
819352,
820000,
820128
]
}
},
"shared": {
"phase_ms": {
"median": 1180.588,
"samples": [
869.228,
842.213,
1180.588,
1560.087,
1728.575
]
},
"total_ms": {
"median": 2260.709,
"samples": [
1632.489,
1553.753,
2260.709,
2879.728,
3388.639
]
},
"cpu_ms": {
"median": 1603.393,
"samples": [
1568.288,
1432.106,
1603.393,
1604.423,
1619.02
]
},
"peak_rss_kib": {
"median": 351728,
"samples": [
353256,
353664,
351728,
350724,
351668
]
}
}
},
"build": "cargo build --release -p horizon-client --example shared_json_startup_profile",
"limitations": "Loader-only wall-clock timings under concurrent machine load; not full startup or time to first frame. RSS includes Assets::load."
}
@@ -1,227 +0,0 @@
{
"scope": "Assets + block facts + behaviours loader only; excludes full startup",
"resources": ".local/horizon-client/26.3",
"repeats": 5,
"metrics": {
"baseline": {
"phase_ms": {
"median": 1996.921,
"samples": [
2213.335,
1453.207,
1637.901,
1996.921,
2010.335
],
"range": [
1453.207,
2213.335
]
},
"total_ms": {
"median": 4728.136,
"samples": [
5628.975,
2147.329,
2162.696,
4728.136,
6404.524
],
"range": [
2147.329,
6404.524
]
},
"cpu_ms": {
"median": 2514.776,
"samples": [
2919.908,
2088.239,
2212.892,
2716.104,
2514.776
],
"range": [
2088.239,
2919.908
]
},
"peak_rss_kib": {
"median": 819824,
"samples": [
819824,
820872,
820744,
818980,
818816
]
}
},
"pr607": {
"phase_ms": {
"median": 586.707,
"samples": [
464.424,
739.67,
586.707,
1373.307,
509.266
],
"range": [
464.424,
1373.307
]
},
"total_ms": {
"median": 1128.6,
"samples": [
954.73,
1764.614,
1128.6,
4312.528,
1030.735
],
"range": [
954.73,
4312.528
]
},
"cpu_ms": {
"median": 1145.826,
"samples": [
981.058,
1465.79,
1145.826,
1736.807,
1056.992
],
"range": [
981.058,
1736.807
]
},
"peak_rss_kib": {
"median": 351900,
"samples": [
352164,
351816,
351900,
352348,
350492
]
}
},
"shared": {
"phase_ms": {
"median": 560.117,
"samples": [
522.284,
1255.75,
560.117,
2151.064,
479.329
],
"range": [
479.329,
2151.064
]
},
"total_ms": {
"median": 1101.305,
"samples": [
1011.715,
2565.449,
1101.305,
2960.186,
1006.817
],
"range": [
1006.817,
2960.186
]
},
"cpu_ms": {
"median": 1128.214,
"samples": [
1035.19,
1565.383,
1128.214,
1494.36,
1031.198
],
"range": [
1031.198,
1565.383
]
},
"peak_rss_kib": {
"median": 351532,
"samples": [
351624,
350540,
351532,
351716,
350076
]
}
},
"deferred": {
"phase_ms": {
"median": 564.046,
"samples": [
433.35,
665.149,
587.456,
564.046,
497.422
],
"range": [
433.35,
665.149
]
},
"total_ms": {
"median": 1143.129,
"samples": [
885.053,
1143.129,
1195.869,
1074.27,
1162.103
],
"range": [
885.053,
1195.869
]
},
"cpu_ms": {
"median": 1114.261,
"samples": [
910.946,
1128.96,
1194.638,
1114.261,
1073.823
],
"range": [
910.946,
1194.638
]
},
"peak_rss_kib": {
"median": 273776,
"samples": [
274320,
273860,
272548,
273776,
272520
]
}
}
},
"baseline_commit": "e422bd13",
"pr607_commit": "a01d47f2",
"measurement": "same optimized binary, isolated processes in alternating forward/reverse four-mode runs",
"limitations": "Includes Assets::load, BlockFacts, Behaviours; excludes complete client startup, pack/mod root resolution, graphics and first frame. Different modes emulate the original independent, PR #607 at a01d47f2, paired and deferred startup sequences in the same release build. Machine was not idle. Elapsed times show high variance; no statistically established gain between PR, paired and deferred modes."
}
@@ -1,453 +0,0 @@
{
"scope": "Assets + BlockFacts + Behaviours loader only; no full startup or first frame",
"resources": "/srv/bob-data/horizon/.local/horizon-client/26.3",
"repeats": 8,
"ordering": [
[
"cf837118",
"cd61e11b",
"arc-key",
"reused-sounds",
"current"
],
[
"current",
"reused-sounds",
"arc-key",
"cd61e11b",
"cf837118"
],
[
"cf837118",
"cd61e11b",
"arc-key",
"reused-sounds",
"current"
],
[
"current",
"reused-sounds",
"arc-key",
"cd61e11b",
"cf837118"
],
[
"cf837118",
"cd61e11b",
"arc-key",
"reused-sounds",
"current"
],
[
"current",
"reused-sounds",
"arc-key",
"cd61e11b",
"cf837118"
],
[
"cf837118",
"cd61e11b",
"arc-key",
"reused-sounds",
"current"
],
[
"current",
"reused-sounds",
"arc-key",
"cd61e11b",
"cf837118"
]
],
"load_average_before": [
10.9873046875,
8.37646484375,
6.91015625
],
"load_average_after": [
8.6962890625,
8.1591796875,
6.90087890625
],
"executables": {
"cf837118": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-cf837118",
"mode": "deferred",
"sha256": "f1b044cd9ac2f67c644fb40bd7ecbe38128ec739a8112d57fc36f05fe9036a52"
},
"cd61e11b": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-cd61e11b",
"mode": "deferred",
"sha256": "fb2d3725b09ee38f85b49901f7053a6fe339b980a2b2dac6930a8ded22c139e8"
},
"arc-key": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-arcpool",
"mode": "deferred",
"sha256": "8cbb53d01afedf5c7a5da0e36c9f8c3569f34def9bc51488fe8699e7f615a131"
},
"reused-sounds": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-reusesounds",
"mode": "deferred",
"sha256": "f45794bfbdbb3f9b5d34c375d397191ac67984b3ae91268aa3fdd02f2c1b7b98"
},
"current": {
"path": "/srv/bob-data/horizon/.worktrees/shared-block-json-20261017/target/release/examples/shared_json_startup_profile-new",
"mode": "deferred",
"sha256": "84eb1baae8e138f52e300b0b44f1339fca38e70db6e06126e78db776ea3e5cd0"
}
},
"metrics": {
"cf837118": {
"phase_ms": {
"median": 424.082,
"p25": 417.508,
"p75": 430.923,
"min": 395.439,
"max": 948.609,
"samples": [
948.609,
425.085,
425.571,
417.945,
395.439,
416.197,
423.078,
446.98
]
},
"total_ms": {
"median": 853.026,
"p25": 839.421,
"p75": 867.731,
"min": 813.144,
"max": 1906.081,
"samples": [
1906.081,
861.641,
864.233,
844.411,
813.144,
830.783,
842.3,
878.226
]
},
"cpu_ms": {
"median": 876.143,
"p25": 862.934,
"p75": 891.4,
"min": 837.769,
"max": 1517.372,
"samples": [
1517.372,
885.13,
887.888,
865.404,
837.769,
855.526,
867.157,
901.937
]
},
"peak_rss_kib": {
"median": 273904.0,
"p25": 273717.0,
"p75": 274142.0,
"min": 273648,
"max": 274836,
"samples": [
273648,
273936,
273872,
274836,
274064,
274376,
273740,
273648
]
}
},
"cd61e11b": {
"phase_ms": {
"median": 422.356,
"p25": 406.294,
"p75": 425.846,
"min": 394.111,
"max": 472.536,
"samples": [
425.832,
420.666,
472.536,
425.886,
400.588,
394.111,
424.047,
408.196
]
},
"total_ms": {
"median": 852.475,
"p25": 828.474,
"p75": 876.413,
"min": 817.3,
"max": 951.587,
"samples": [
951.587,
865.442,
899.564,
868.696,
823.74,
817.3,
839.508,
830.052
]
},
"cpu_ms": {
"median": 875.04,
"p25": 850.012,
"p75": 900.57,
"min": 837.672,
"max": 938.285,
"samples": [
938.285,
888.533,
926.846,
891.812,
845.956,
837.672,
861.548,
851.364
]
},
"peak_rss_kib": {
"median": 268086.0,
"p25": 267719.0,
"p75": 268486.0,
"min": 267444,
"max": 268920,
"samples": [
267776,
268660,
267752,
268920,
268428,
268396,
267444,
267620
]
}
},
"arc-key": {
"phase_ms": {
"median": 421.422,
"p25": 401.781,
"p75": 451.774,
"min": 394.041,
"max": 472.337,
"samples": [
446.155,
472.337,
468.632,
420.636,
400.91,
394.041,
422.207,
402.072
]
},
"total_ms": {
"median": 866.601,
"p25": 822.563,
"p75": 918.038,
"min": 817.092,
"max": 1001.287,
"samples": [
1001.287,
918.457,
917.898,
892.666,
817.092,
824.225,
840.536,
817.576
]
},
"cpu_ms": {
"median": 887.84,
"p25": 844.533,
"p75": 937.331,
"min": 838.687,
"max": 991.212,
"samples": [
991.212,
949.048,
933.425,
911.921,
838.687,
845.021,
863.759,
843.069
]
},
"peak_rss_kib": {
"median": 267728.0,
"p25": 267598.0,
"p75": 268039.0,
"min": 267452,
"max": 268636,
"samples": [
268264,
267616,
267840,
267964,
268636,
267452,
267580,
267604
]
}
},
"reused-sounds": {
"phase_ms": {
"median": 421.683,
"p25": 412.913,
"p75": 439.098,
"min": 395.338,
"max": 487.446,
"samples": [
487.446,
434.111,
402.815,
416.279,
424.524,
454.058,
395.338,
418.842
]
},
"total_ms": {
"median": 879.846,
"p25": 845.021,
"p75": 901.166,
"min": 823.721,
"max": 1013.539,
"samples": [
1013.539,
892.033,
928.565,
884.284,
847.853,
875.407,
823.721,
836.527
]
},
"cpu_ms": {
"median": 894.546,
"p25": 871.499,
"p75": 921.844,
"min": 844.891,
"max": 1028.688,
"samples": [
1028.688,
913.48,
946.935,
903.785,
868.684,
885.308,
844.891,
872.438
]
},
"peak_rss_kib": {
"median": 267960.0,
"p25": 267678.0,
"p75": 268133.0,
"min": 267212,
"max": 268404,
"samples": [
267212,
267888,
268404,
267692,
268096,
267636,
268244,
268032
]
}
},
"current": {
"phase_ms": {
"median": 411.403,
"p25": 406.814,
"p75": 452.869,
"min": 391.768,
"max": 532.402,
"samples": [
532.402,
442.563,
408.857,
410.202,
483.788,
391.768,
400.683,
412.604
]
},
"total_ms": {
"median": 905.017,
"p25": 851.488,
"p75": 937.934,
"min": 818.206,
"max": 1075.445,
"samples": [
1075.445,
878.155,
837.627,
955.655,
932.027,
818.206,
856.108,
931.88
]
},
"cpu_ms": {
"median": 922.9,
"p25": 874.849,
"p75": 956.282,
"min": 839.639,
"max": 1067.693,
"samples": [
1067.693,
900.18,
862.137,
970.356,
951.591,
839.639,
879.086,
945.62
]
},
"peak_rss_kib": {
"median": 268354.0,
"p25": 267889.0,
"p75": 268492.0,
"min": 267056,
"max": 268820,
"samples": [
268008,
268384,
268564,
268820,
267056,
267532,
268324,
268468
]
}
}
},
"experiment_note": "Exploratory variants (Arc-key pool, reused sounds, combined proposal) alongside cd61e11b; none gave a robust consistent improvement and all were reverted."
}
-14
View File
@@ -1,14 +0,0 @@
[package]
name = "cargo-horizon-mod"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.86"
description = "cargo horizon-mod new/build/run: make Horizon mods in Rust (docs/mods.md)"
[dependencies]
horizon-mods = { path = "../horizon-mods", default-features = false, features = ["pack"] }
serde_json = "1.0.151"
[lints]
workspace = true
-201
View File
@@ -1,201 +0,0 @@
//! `cargo horizon-mod`: make Horizon mods in Rust (`docs/mods.md`).
//!
//! - `new <path> [--name "Name"] [--sdk-path <dir>]`: a mod crate from the template.
//! - `build [--debug]`: the `.hmod` in `target/horizon-mod/`.
//! - `run [--game <horizon-client>] [-- <game args>]`: build, then play with
//! the mod; run `build` again and the game reloads it.
use horizon_mods::{
Manifest, Package,
manifest::{MANIFEST, component_imports, componentize},
};
use std::{
path::{Path, PathBuf},
process::{Command, ExitCode, Stdio},
};
const TARGET: &str = "wasm32-unknown-unknown";
const SDK_GIT: &str = "{ git = \"https://github.com/ludovic111/horizon\" }";
fn main() -> ExitCode {
// Cargo runs `cargo-horizon-mod horizon-mod <args>`.
let mut args: Vec<String> = std::env::args().skip(1).collect();
if args.first().is_some_and(|a| a == "horizon-mod") {
args.remove(0);
}
let result = match args.first().map(String::as_str) {
Some("new") => new(&args[1..]),
Some("build") => build(!args.iter().any(|a| a == "--debug")).map(|p| {
println!("Built {}", p.display());
}),
Some("run") => run(&args[1..]),
_ => Err("usage: cargo horizon-mod new <path> | build [--debug] | run [--game <horizon-client>] [-- <game args>]".into()),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
/// The value after `flag` in `args`.
fn flag<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
let i = args.iter().position(|a| a == flag)?;
args.get(i + 1).map(String::as_str)
}
fn new(args: &[String]) -> Result<(), String> {
let path = args
.first()
.filter(|a| !a.starts_with("--"))
.ok_or("cargo horizon-mod new <path>")?;
let dir = Path::new(path);
if dir.exists() {
return Err(format!("{} already exists", dir.display()));
}
let base = dir
.file_name()
.and_then(|n| n.to_str())
.ok_or("the path needs a folder name")?;
let id: String = base
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'_'
}
})
.collect();
let name = flag(args, "--name").map_or_else(|| base.replace(['-', '_'], " "), str::to_string);
let sdk = match flag(args, "--sdk-path") {
Some(p) => {
let p = std::fs::canonicalize(p).map_err(|e| format!("--sdk-path {p}: {e}"))?;
format!("{{ path = {:?} }}", p.display().to_string())
}
None => SDK_GIT.to_string(),
};
let fill = |t: &str| {
t.replace("{crate}", &id.replace('_', "-"))
.replace("{id}", &id)
.replace("{name}", &name)
.replace("{sdk}", &sdk)
};
let files = [
("Cargo.toml", include_str!("../template/Cargo.toml.in")),
(MANIFEST, include_str!("../template/horizon-mod.toml")),
("src/lib.rs", include_str!("../template/src/lib.rs")),
(".gitignore", include_str!("../template/gitignore")),
];
for (file, text) in files {
let to = dir.join(file);
std::fs::create_dir_all(to.parent().expect("a file in the mod"))
.map_err(|e| e.to_string())?;
std::fs::write(&to, fill(text)).map_err(|e| format!("{}: {e}", to.display()))?;
}
Manifest::parse(&std::fs::read_to_string(dir.join(MANIFEST)).map_err(|e| e.to_string())?)?;
println!(
"Created {} (mod id {id}). Next: cd {path} && cargo horizon-mod run",
dir.display()
);
Ok(())
}
/// Builds the crate in the current folder and packs `target/horizon-mod/<id>.hmod`.
fn build(release: bool) -> Result<PathBuf, String> {
let manifest_text = std::fs::read_to_string(MANIFEST)
.map_err(|e| format!("{MANIFEST}: {e} (run this in a mod's folder)"))?;
let manifest = Manifest::parse(&manifest_text)?;
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(cargo);
cmd.args([
"build",
"--lib",
"--target",
TARGET,
"--message-format=json-render-diagnostics",
]);
if release {
cmd.arg("--release");
}
let out = cmd
.stderr(Stdio::inherit())
.output()
.map_err(|e| e.to_string())?;
if !out.status.success() {
return Err(format!(
"cargo build failed (is the target installed? rustup target add {TARGET})"
));
}
let core = String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
.filter(|m| m["reason"] == "compiler-artifact")
.flat_map(|m| m["filenames"].as_array().cloned().unwrap_or_default())
.filter_map(|f| f.as_str().map(PathBuf::from))
.rfind(|f| f.extension().is_some_and(|e| e == "wasm"))
.ok_or("the build made no .wasm: is the crate a cdylib?")?;
let wasm = componentize(&std::fs::read(&core).map_err(|e| e.to_string())?)?;
// The game refuses a component importing capabilities the manifest lacks.
manifest.check_imports(component_imports(&wasm)?.iter().map(String::as_str))?;
let target_dir = core.ancestors().nth(3).ok_or("unexpected target folder")?;
let dir = target_dir.join("horizon-mod");
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let path = dir.join(format!("{}.hmod", manifest.id));
// Write then rename, so a running game never reads half a file.
let partial = path.with_extension("hmod.partial");
std::fs::write(&partial, Package::write(&manifest_text, &wasm)?).map_err(|e| e.to_string())?;
std::fs::rename(&partial, &path).map_err(|e| e.to_string())?;
Ok(path)
}
fn run(args: &[String]) -> Result<(), String> {
let path = build(true)?;
let game = flag(args, "--game")
.map(str::to_string)
.or_else(|| std::env::var("HORIZON_CLIENT").ok())
.unwrap_or_else(|| "horizon-client".into());
let extra = args.iter().skip_while(|a| *a != "--").skip(1);
println!(
"Starting {game} with {} (rebuild with `cargo horizon-mod build` to reload it)",
path.display()
);
let status = Command::new(&game)
.arg("--dev-mod")
.arg(&path)
.args(extra)
.status()
.map_err(|e| {
format!("{game}: {e} (pass --game <path to horizon-client> or set HORIZON_CLIENT)")
})?;
status
.success()
.then_some(())
.ok_or_else(|| format!("the game exited with {status}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_template_is_a_valid_mod() {
let dir = std::env::temp_dir().join(format!("horizon-mod-new-{}", std::process::id()));
let path = dir.join("My-Mod");
new(&[path.display().to_string(), "--name".into(), "My Mod".into()]).unwrap();
let manifest =
Manifest::parse(&std::fs::read_to_string(path.join(MANIFEST)).unwrap()).unwrap();
assert_eq!(
(manifest.id.as_str(), manifest.name.as_str()),
("my_mod", "My Mod")
);
let cargo = std::fs::read_to_string(path.join("Cargo.toml")).unwrap();
assert!(cargo.contains("name = \"my-mod\"") && cargo.contains(SDK_GIT));
assert!(
new(&[path.display().to_string()]).is_err(),
"never overwrites"
);
std::fs::remove_dir_all(&dir).unwrap();
}
}
@@ -1,14 +0,0 @@
[package]
name = "{crate}"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
horizon-mod-sdk = {sdk}
[profile.release]
opt-level = "s"
lto = true
@@ -1 +0,0 @@
/target
@@ -1,12 +0,0 @@
id = "{id}"
name = "{name}"
version = "0.1.0"
description = ""
authors = []
# The Horizon mod API the SDK targets.
api = "0.1"
# client: runs in the player's game.
side = "client"
# What the mod may do, shown to players before they turn it on:
# game-read (position, health, time), chat (show messages), hud (draw text).
capabilities = ["chat"]
@@ -1,19 +0,0 @@
use horizon_mod_sdk::{Mod, Registration, chat};
struct MyMod {
greeted: u32,
}
impl Mod for MyMod {
fn init() -> Result<(Self, Registration), String> {
Ok((MyMod { greeted: 0 }, Registration::new().command("hello")))
}
fn command(&mut self, _name: &str, args: &str) -> Result<Option<String>, String> {
self.greeted += 1;
chat::show(&format!("Hello {args}! ({} so far)", self.greeted));
Ok(None)
}
}
horizon_mod_sdk::export_mod!(MyMod);
@@ -82,19 +82,6 @@ impl CompiledChoices {
.collect::<Result<_>>()?,
})
}
/// The variant `particleMaterial` comes from: the first entry of a weighted
/// list (`WeightedVariants`) and, for a multipart root, of its first
/// selector whether or not that selector applies (`MultiPartModel`).
pub fn particle_variant(&self, selected: &SelectedModels) -> Option<&Variant> {
let choice = match selected {
SelectedModels::Variant(index) => self.variants.get(*index)?,
SelectedModels::Multipart(_) => self.multipart.first()?,
};
Some(match choice {
Choice::Single(value) => value,
Choice::Weighted { variants, .. } => variants.first()?,
})
}
/// Append selected variants without allocating intermediate part lists. A
/// multipart root consumes nextLong even for an empty selected subset, then
/// resets the same random source to that seed before each selected part.
+16 -34
View File
@@ -50,47 +50,29 @@ impl ModelLibrary {
/// A library that also knows `builtin/generated`, the parent of every
/// flat item model; the native client registers it as a special model.
pub fn with_item_generator(mut models: BTreeMap<String, CuboidModel>) -> Result<Self> {
models.insert(GENERATED_MODEL.to_owned(), Self::item_generator_model());
Self::new(models)
}
/// Register the item built-in while reusing already decoded resources.
pub fn with_shared_item_generator(
mut models: BTreeMap<String, Arc<CuboidModel>>,
) -> Result<Self> {
models.insert(
GENERATED_MODEL.to_owned(),
Arc::new(Self::item_generator_model()),
CuboidModel {
parent: None,
ambient_occlusion: None,
gui_light: Some(GuiLight::Front),
textures: BTreeMap::from([(
"particle".to_owned(),
TextureSlot::Reference("layer0".to_owned()),
)]),
elements: None,
display: None,
},
);
Self::from_shared(models)
}
fn item_generator_model() -> CuboidModel {
CuboidModel {
parent: None,
ambient_occlusion: None,
gui_light: Some(GuiLight::Front),
textures: BTreeMap::from([(
"particle".to_owned(),
TextureSlot::Reference("layer0".to_owned()),
)]),
elements: None,
display: None,
}
Self::new(models)
}
pub fn new(models: BTreeMap<String, CuboidModel>) -> Result<Self> {
Self::from_models(
models
.into_iter()
.map(|(name, model)| (name, Arc::new(model))),
)
}
/// Build an immutable resolution view without copying model geometry.
pub fn from_shared(models: BTreeMap<String, Arc<CuboidModel>>) -> Result<Self> {
Self::from_models(models)
}
fn from_models(models: impl IntoIterator<Item = (String, Arc<CuboidModel>)>) -> Result<Self> {
let mut decoded = BTreeMap::new();
for (name, model) in models {
if decoded.insert(identifier(&name)?, model).is_some() {
if decoded
.insert(identifier(&name)?, Arc::new(model))
.is_some()
{
return Err(error("duplicate normalized model identifier"));
}
}
-14
View File
@@ -1,14 +0,0 @@
[package]
name = "horizon-bots"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Load-test bots for Minecraft Java Edition 26.3 servers (docs/server.md, section 9)"
[dependencies]
horizon-protocol = { path = "../horizon-protocol" }
serde_json = "1.0.151"
[lints]
workspace = true
-697
View File
@@ -1,697 +0,0 @@
//! `horizon-bots`: load-test bots for any Minecraft Java Edition 26.3 server, vanilla, Paper or Horizon
//! (docs/server.md, section 9). Each bot logs in offline, goes through configuration as vanilla's client
//! does, answers keep-alives and pings, accepts teleports, acknowledges chunk batches, and moves at 20 Hz.
//!
//! Bots fly a little above the ground, so the server must give them creative mode (`gamemode=creative`
//! and `force-gamemode=true`): they never touch blocks, so they need no world of their own, and no server
//! can refuse their moves for walking into terrain. Every behaviour is deterministic from `--seed`.
//!
//! horizon-bots --server 127.0.0.1:25565 --bots 25 --behaviour mixed --seconds 360 --report bots.json
//!
//! It prints one JSON line a second (bots online, chunks received) and writes a summary with `--report`.
//! A bot moves only while it holds the chunk it stands in: vanilla's client stops ticking its player in a chunk
//! it hasn't received, so a player can't outrun the server's chunk sending, on any server.
//! Each bot also keeps the chunks it holds, as a client's chunk cache does, so the report tells chunks that
//! entered the view apart from chunks sent again while the bot still held them (`resent`), and counts the
//! times the server pulled a bot back (`pulled_back`: a teleport after the first, as for "moved too quickly").
use horizon_protocol::{
PROTOCOL_VERSION,
buf::{Reader, Writer},
connection::{ConnWriter, Connection},
crypto::offline_uuid,
ids,
packets::{
Empty, configuration,
handshake::{self, Intention},
login::{self, ServerHello},
play,
},
};
use serde_json::json;
use std::{
collections::HashSet,
sync::{
Arc, Mutex,
atomic::{AtomicBool, AtomicU64, Ordering},
},
thread,
time::{Duration, Instant},
};
const HELP: &str = "\
horizon-bots: load-test bots for a Minecraft Java Edition 26.3 server (offline mode, creative)
--server HOST:PORT default 127.0.0.1:25565
--bots N default 10
--behaviour B idle, walk, explore or mixed (60 % walk, 20 % explore, 20 % idle); default mixed
--join-every SECONDS default 1
--seconds N how long to run after the first bot joins; default 60
--view-distance N what the bots ask for; default 10
--seed N default 12345
--explore-speed B blocks a second an explore bot flies; default 22 (as fast as chunks come)
--name PREFIX bot names: PREFIX0, PREFIX1...; default Bot
--report FILE a JSON summary at the end (- prints it after the per-second lines)
";
#[derive(Clone, Copy, Debug, PartialEq)]
enum Behaviour {
Idle,
Walk,
Explore,
}
impl Behaviour {
fn name(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Walk => "walk",
Self::Explore => "explore",
}
}
}
struct Options {
host: String,
port: u16,
bots: usize,
behaviour: String,
join_every: f64,
seconds: u64,
view_distance: i8,
seed: u64,
explore_speed: f64,
name: String,
report: Option<String>,
}
fn options() -> Result<Options, String> {
let mut o = Options {
host: "127.0.0.1".into(),
port: 25565,
bots: 10,
behaviour: "mixed".into(),
join_every: 1.0,
seconds: 60,
view_distance: 10,
seed: 12345,
explore_speed: 22.0,
name: "Bot".into(),
report: None,
};
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if arg == "-h" || arg == "--help" {
print!("{HELP}");
std::process::exit(0);
}
let value = args.next().ok_or(format!("{arg} needs a value"))?;
let number = |what: &str| format!("{arg} needs {what}, not {value:?}");
match arg.as_str() {
"--server" => {
let (host, port) = value.rsplit_once(':').unwrap_or((&value, "25565"));
o.host = host.into();
o.port = port.parse().map_err(|_| number("a port"))?;
}
"--bots" => o.bots = value.parse().map_err(|_| number("a number"))?,
"--behaviour" => o.behaviour = value,
"--join-every" => o.join_every = value.parse().map_err(|_| number("seconds"))?,
"--seconds" => o.seconds = value.parse().map_err(|_| number("seconds"))?,
"--view-distance" => o.view_distance = value.parse().map_err(|_| number("2 to 32"))?,
"--seed" => o.seed = value.parse().map_err(|_| number("a number"))?,
"--explore-speed" => {
o.explore_speed = value.parse().map_err(|_| number("blocks a second"))?;
}
"--name" => o.name = value,
"--report" => o.report = Some(value),
_ => return Err(format!("unknown option {arg}; --help lists them")),
}
}
if !["idle", "walk", "explore", "mixed"].contains(&o.behaviour.as_str()) {
return Err(format!(
"--behaviour {}: use idle, walk, explore or mixed",
o.behaviour
));
}
Ok(o)
}
/// What one bot is doing, shared between its reader and its ticks.
#[derive(Default)]
struct Bot {
online: AtomicBool,
chunks: AtomicU64,
/// Chunks received while the bot already held them (no ForgetLevelChunk in between).
resent: AtomicU64,
/// Different chunks received over the run.
unique: AtomicU64,
forgotten: AtomicU64,
/// Teleports after the spawn one: the server refused a move.
pulled_back: AtomicU64,
/// Ticks the bot couldn't move because it didn't hold its chunk yet.
waited: AtomicU64,
/// The chunks the bot holds (ClientChunkCache).
held: Mutex<HashSet<(i32, i32)>>,
/// Where the server last put the bot (a teleport), to start from.
teleport: Mutex<Option<[f64; 3]>>,
error: Mutex<Option<String>>,
joined_after_ms: AtomicU64,
}
/// A small deterministic generator (SplitMix64), so every run moves the bots the same way.
struct Random(u64);
impl Random {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn unit(&mut self) -> f64 {
(self.next() >> 11) as f64 / (1u64 << 53) as f64
}
}
fn raw(id: i32, write: impl FnOnce(&mut Writer)) -> Vec<u8> {
let mut w = Writer::new();
w.varint(id);
write(&mut w);
w.data
}
fn err(e: impl std::fmt::Display) -> String {
e.to_string()
}
/// Handshake, offline login and configuration; returns the connection in the play state.
fn join(o: &Options, name: &str) -> Result<Connection, String> {
let mut c = Connection::connect(&o.host, o.port, Duration::from_secs(10)).map_err(err)?;
c.send(
&handshake::Serverbound::Intention(Intention {
protocol: PROTOCOL_VERSION,
host: o.host.clone(),
port: o.port,
intent: handshake::LOGIN,
})
.encode(),
)
.map_err(err)?;
c.send(
&login::Serverbound::Hello(ServerHello {
name: name.into(),
uuid: offline_uuid(name),
})
.encode(),
)
.map_err(err)?;
loop {
let (id, body) = c.recv().map_err(err)?;
match login::Clientbound::decode(id, &body).map_err(err)? {
login::Clientbound::Compression(p) => c.set_compression(p.threshold),
login::Clientbound::LoginFinished(_) => {
c.send(&login::Serverbound::LoginAcknowledged(Empty).encode())
.map_err(err)?;
break;
}
login::Clientbound::Disconnect(p) => return Err(format!("refused: {}", p.reason_json)),
login::Clientbound::Hello(_) => {
return Err("the server is in online mode; bots need online-mode=false".into());
}
login::Clientbound::CustomQuery(p) => c
.send(
&login::Serverbound::CustomQueryAnswer(login::CustomQueryAnswer {
transaction: p.transaction,
payload: None,
})
.encode(),
)
.map_err(err)?,
login::Clientbound::CookieRequest(p) => c
.send(
&login::Serverbound::CookieResponse(login::CookieResponse {
key: p.key,
payload: None,
})
.encode(),
)
.map_err(err)?,
login::Clientbound::Unknown(..) => {}
}
}
use ids::configuration::clientbound as cb;
c.send(
&configuration::Serverbound::ClientInformation(configuration::ClientInformation::new(
o.view_distance,
))
.encode(),
)
.map_err(err)?;
loop {
let (id, body) = c.recv().map_err(err)?;
let reply = match id {
cb::SELECT_KNOWN_PACKS
| cb::KEEP_ALIVE
| cb::PING
| cb::CODE_OF_CONDUCT
| cb::COOKIE_REQUEST
| cb::DISCONNECT
| cb::FINISH_CONFIGURATION => {
match configuration::Clientbound::decode(id, &body).map_err(err)? {
configuration::Clientbound::SelectKnownPacks(p) => {
configuration::Serverbound::SelectKnownPacks(p)
}
configuration::Clientbound::KeepAlive(p) => {
configuration::Serverbound::KeepAlive(p)
}
configuration::Clientbound::Ping(p) => configuration::Serverbound::Pong(p),
configuration::Clientbound::CodeOfConduct(_) => {
configuration::Serverbound::AcceptCodeOfConduct(Empty)
}
configuration::Clientbound::CookieRequest(p) => {
configuration::Serverbound::CookieResponse(configuration::CookieResponse {
key: p.key,
payload: None,
})
}
configuration::Clientbound::Disconnect(_) => {
return Err("disconnected during configuration".into());
}
configuration::Clientbound::FinishConfiguration(_) => {
c.send(&configuration::Serverbound::FinishConfiguration(Empty).encode())
.map_err(err)?;
return Ok(c);
}
_ => continue,
}
}
_ => continue,
};
c.send(&reply.encode()).map_err(err)?;
}
}
/// ChunkBatchSizeCalculator: the chunks per tick a vanilla client asks for, from how long its batches take
/// to arrive.
struct BatchSize {
nanos_per_chunk: f64,
old_samples_weight: u32,
started: Instant,
}
impl BatchSize {
fn new() -> Self {
Self {
nanos_per_chunk: 2_000_000.0,
old_samples_weight: 1,
started: Instant::now(),
}
}
fn finished(&mut self, size: i32) {
if size > 0 {
let per_chunk = self.started.elapsed().as_nanos() as f64 / f64::from(size);
let clamped = per_chunk.clamp(self.nanos_per_chunk / 3.0, self.nanos_per_chunk * 3.0);
let weight = f64::from(self.old_samples_weight);
self.nanos_per_chunk = (self.nanos_per_chunk * weight + clamped) / (weight + 1.0);
self.old_samples_weight = (self.old_samples_weight + 1).min(49);
}
}
fn desired_chunks_per_tick(&self) -> f32 {
(7_000_000.0 / self.nanos_per_chunk) as f32
}
}
/// Reads the play state: answers what a client must answer, counts chunks, records teleports.
fn read(
mut reader: horizon_protocol::connection::ConnReader,
writer: Arc<Mutex<ConnWriter>>,
bot: Arc<Bot>,
) {
use ids::play::clientbound as cb;
use ids::play::serverbound as sb;
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body);
let mut seen = HashSet::new();
let mut batch = BatchSize::new();
let mut teleports = 0u64;
loop {
let (id, body) = match reader.recv() {
Ok(packet) => packet,
Err(e) => {
if bot.online.swap(false, Ordering::SeqCst) {
*bot.error.lock().unwrap() = Some(e.to_string());
}
return;
}
};
let mut r = Reader::new(&body);
let sent = match id {
cb::KEEP_ALIVE => match r.i64() {
Ok(v) => send(raw(sb::KEEP_ALIVE, |w| w.i64(v))),
Err(_) => Ok(()),
},
cb::PING => match r.i32() {
Ok(v) => send(raw(sb::PONG, |w| w.i32(v))),
Err(_) => Ok(()),
},
cb::LEVEL_CHUNK_WITH_LIGHT => {
bot.chunks.fetch_add(1, Ordering::Relaxed);
if let (Ok(x), Ok(z)) = (r.i32(), r.i32()) {
if !bot.held.lock().unwrap().insert((x, z)) {
bot.resent.fetch_add(1, Ordering::Relaxed);
}
if seen.insert((x, z)) {
bot.unique.fetch_add(1, Ordering::Relaxed);
}
}
Ok(())
}
cb::FORGET_LEVEL_CHUNK => {
if let Ok(play::Clientbound::ForgetLevelChunk(p)) =
play::Clientbound::decode(id, &body)
&& bot.held.lock().unwrap().remove(&(p.pos[0], p.pos[1]))
{
bot.forgotten.fetch_add(1, Ordering::Relaxed);
}
Ok(())
}
cb::CHUNK_BATCH_START => {
batch.started = Instant::now();
Ok(())
}
cb::CHUNK_BATCH_FINISHED => {
batch.finished(r.varint().unwrap_or(0));
send(
play::Serverbound::ChunkBatchReceived(play::ChunkBatchReceived {
chunks_per_tick: batch.desired_chunks_per_tick(),
})
.encode(),
)
}
cb::PLAYER_POSITION => match play::Clientbound::decode(id, &body) {
Ok(play::Clientbound::PlayerPosition(p)) => {
let mut at = bot.teleport.lock().unwrap();
let previous = at.unwrap_or([0.0; 3]);
// Relative flags: X 1, Y 2, Z 4.
let pos: [f64; 3] = std::array::from_fn(|i| {
if p.relatives & (1 << i) != 0 {
previous[i] + p.position[i]
} else {
p.position[i]
}
});
*at = Some(pos);
teleports += 1;
if teleports > 1 {
bot.pulled_back.fetch_add(1, Ordering::Relaxed);
}
send(
play::Serverbound::AcceptTeleportation(play::AcceptTeleportation {
id: p.teleport,
x: pos[0],
y: pos[1],
z: pos[2],
yaw: p.yaw,
pitch: p.pitch,
})
.encode(),
)
}
_ => Ok(()),
},
cb::DISCONNECT | cb::START_CONFIGURATION => {
bot.online.store(false, Ordering::SeqCst);
*bot.error.lock().unwrap() = Some(if id == cb::DISCONNECT {
format!(
"disconnected: {}",
String::from_utf8_lossy(&body)
.chars()
.filter(|c| !c.is_control())
.collect::<String>()
)
} else {
"sent back to configuration".into()
});
return;
}
_ => Ok(()),
};
if sent.is_err() {
bot.online.store(false, Ordering::SeqCst);
return;
}
}
}
/// One bot: join, then move every 50 ms until `until`.
fn run_bot(
o: Arc<Options>,
index: usize,
behaviour: Behaviour,
bot: Arc<Bot>,
until: Arc<Mutex<Option<Instant>>>,
) {
let name = format!("{}{index}", o.name);
let asked = Instant::now();
let connection = match join(&o, &name) {
Ok(c) => c,
Err(e) => {
*bot.error.lock().unwrap() = Some(e);
return;
}
};
let (reader, writer) = connection.split();
let writer = Arc::new(Mutex::new(writer));
bot.online.store(true, Ordering::SeqCst);
{
let (writer, bot) = (writer.clone(), bot.clone());
thread::spawn(move || read(reader, writer, bot));
}
use ids::play::serverbound as sb;
// Wait for the spawn position.
let spawn = loop {
if let Some(at) = *bot.teleport.lock().unwrap() {
break at;
}
if !bot.online.load(Ordering::SeqCst) || asked.elapsed() > Duration::from_secs(60) {
if bot.error.lock().unwrap().is_none() {
*bot.error.lock().unwrap() = Some("no spawn position within 60 s".into());
}
return;
}
thread::sleep(Duration::from_millis(20));
};
bot.joined_after_ms
.store(asked.elapsed().as_millis() as u64, Ordering::SeqCst);
let send = |body: Vec<u8>| writer.lock().unwrap().send(&body).is_ok();
send(raw(sb::PLAYER_LOADED, |_| {}));
let mut random = Random(o.seed ^ (index as u64).wrapping_mul(0x2545_F491_4F6C_DD1D));
let mut pos = spawn;
let cruise = spawn[1] + 24.0;
let heading = random.unit() * std::f64::consts::TAU;
let mut target = pos;
let mut next = Instant::now();
loop {
if until.lock().unwrap().is_some_and(|u| Instant::now() >= u)
|| !bot.online.load(Ordering::SeqCst)
{
break;
}
// A teleport from the server wins over where the bot thought it was.
if let Some(at) = bot.teleport.lock().unwrap().take() {
pos = at;
}
let here = ((pos[0].floor() as i32) >> 4, (pos[2].floor() as i32) >> 4);
if !bot.held.lock().unwrap().contains(&here) {
bot.waited.fetch_add(1, Ordering::Relaxed);
} else if pos[1] < cruise {
pos[1] = (pos[1] + 0.5).min(cruise);
} else {
match behaviour {
Behaviour::Idle => {}
Behaviour::Walk => {
let (dx, dz) = (target[0] - pos[0], target[2] - pos[2]);
let distance = dx.hypot(dz);
if distance < 1.0 {
let angle = random.unit() * std::f64::consts::TAU;
let reach = 8.0 + random.unit() * 40.0;
target = [
spawn[0] + angle.cos() * reach,
cruise,
spawn[2] + angle.sin() * reach,
];
} else {
// Sprinting: 5.6 blocks a second.
let step = 0.28_f64.min(distance);
pos[0] += dx / distance * step;
pos[2] += dz / distance * step;
}
}
Behaviour::Explore => {
// Flying in creative, by default about 22 blocks a second, which keeps new
// chunks coming as fast as the server sends them.
let step = o.explore_speed / 20.0;
pos[0] += heading.cos() * step;
pos[2] += heading.sin() * step;
}
}
}
let ok = send(
play::Serverbound::MovePlayerPos(play::MovePlayerPos {
x: pos[0],
y: pos[1],
z: pos[2],
flags: 0,
})
.encode(),
) && send(raw(sb::CLIENT_TICK_END, |_| {}));
if !ok {
bot.online.store(false, Ordering::SeqCst);
break;
}
next += Duration::from_millis(50);
let now = Instant::now();
if next > now {
thread::sleep(next - now);
} else if now - next > Duration::from_secs(1) {
next = now;
}
}
// Offline first, so the reader doesn't count the closed connection as a failure.
bot.online.store(false, Ordering::SeqCst);
writer.lock().unwrap().shutdown();
}
fn behaviour_of(mode: &str, index: usize) -> Behaviour {
match mode {
"idle" => Behaviour::Idle,
"walk" => Behaviour::Walk,
"explore" => Behaviour::Explore,
// mixed: of every five bots, three walk, one explores, one stands.
_ => match index % 5 {
0 | 2 | 4 => Behaviour::Walk,
1 => Behaviour::Explore,
_ => Behaviour::Idle,
},
}
}
fn main() {
let o = match options() {
Ok(o) => Arc::new(o),
Err(e) => {
eprintln!("horizon-bots: {e}");
std::process::exit(2);
}
};
let start = Instant::now();
let until: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
let bots: Vec<Arc<Bot>> = (0..o.bots).map(|_| Arc::new(Bot::default())).collect();
let launcher = {
let (o, bots, until) = (o.clone(), bots.clone(), until.clone());
thread::spawn(move || {
let mut handles = Vec::new();
for (i, bot) in bots.iter().enumerate() {
let (o2, bot, until) = (o.clone(), bot.clone(), until.clone());
let behaviour = behaviour_of(&o.behaviour, i);
handles.push(thread::spawn(move || run_bot(o2, i, behaviour, bot, until)));
thread::sleep(Duration::from_secs_f64(o.join_every));
}
handles
})
};
// The run lasts --seconds after the first bot is in.
let mut previous_chunks = 0;
let mut first = None;
loop {
thread::sleep(Duration::from_secs(1));
let online = bots
.iter()
.filter(|b| b.online.load(Ordering::SeqCst))
.count();
let chunks: u64 = bots.iter().map(|b| b.chunks.load(Ordering::Relaxed)).sum();
let total = |f: fn(&Bot) -> &AtomicU64| -> u64 {
bots.iter().map(|b| f(b).load(Ordering::Relaxed)).sum()
};
let failed = bots
.iter()
.filter(|b| b.error.lock().unwrap().is_some())
.count();
if first.is_none() && online > 0 {
first = Some(Instant::now());
*until.lock().unwrap() = Some(Instant::now() + Duration::from_secs(o.seconds));
}
println!(
"{}",
json!({
"t": start.elapsed().as_secs(),
"online": online,
"failed": failed,
"chunks": chunks,
"chunks_per_second": chunks - previous_chunks,
"unique": total(|b| &b.unique),
"forgotten": total(|b| &b.forgotten),
"resent": total(|b| &b.resent),
"pulled_back": total(|b| &b.pulled_back),
"waited_ticks": total(|b| &b.waited),
})
);
previous_chunks = chunks;
let over = until.lock().unwrap().is_some_and(|u| Instant::now() >= u);
let all_failed = failed == o.bots;
if over || all_failed || (first.is_none() && start.elapsed() > Duration::from_secs(120)) {
*until.lock().unwrap() = Some(Instant::now());
break;
}
}
for handle in launcher.join().unwrap_or_default() {
let _ = handle.join();
}
let summary = json!({
"server": format!("{}:{}", o.host, o.port),
"bots": o.bots,
"behaviour": o.behaviour,
"seed": o.seed,
"explore_speed": o.explore_speed,
"view_distance": o.view_distance,
"seconds": o.seconds,
"joined": bots.iter().filter(|b| b.joined_after_ms.load(Ordering::SeqCst) > 0).count(),
"chunks": bots.iter().map(|b| b.chunks.load(Ordering::Relaxed)).sum::<u64>(),
"resent": bots.iter().map(|b| b.resent.load(Ordering::Relaxed)).sum::<u64>(),
"pulled_back": bots.iter().map(|b| b.pulled_back.load(Ordering::Relaxed)).sum::<u64>(),
"waited_ticks": bots.iter().map(|b| b.waited.load(Ordering::Relaxed)).sum::<u64>(),
"per_bot": bots.iter().enumerate().map(|(i, b)| json!({
"name": format!("{}{i}", o.name),
"behaviour": behaviour_of(&o.behaviour, i).name(),
"joined_after_ms": b.joined_after_ms.load(Ordering::SeqCst),
"chunks": b.chunks.load(Ordering::Relaxed),
"unique_chunks": b.unique.load(Ordering::Relaxed),
"resent": b.resent.load(Ordering::Relaxed),
"forgotten": b.forgotten.load(Ordering::Relaxed),
"pulled_back": b.pulled_back.load(Ordering::Relaxed),
"waited_ticks": b.waited.load(Ordering::Relaxed),
"error": *b.error.lock().unwrap(),
})).collect::<Vec<_>>(),
});
if o.report.as_deref() == Some("-") {
println!("{summary:#}");
} else if let Some(path) = &o.report {
if let Err(e) = std::fs::write(path, format!("{summary:#}\n")) {
eprintln!("horizon-bots: {path}: {e}");
}
}
let errors: Vec<String> = bots
.iter()
.enumerate()
.filter_map(|(i, b)| {
b.error
.lock()
.unwrap()
.clone()
.map(|e| format!("{}{i}: {e}", o.name))
})
.collect();
for e in errors.iter().take(10) {
eprintln!("horizon-bots: {e}");
}
std::process::exit(if errors.is_empty() { 0 } else { 1 });
}
+8 -39
View File
@@ -10,15 +10,9 @@ description = "Horizon native development client"
[dependencies]
horizon-assets = { path = "../horizon-assets" }
horizon-core = { path = "../horizon-core" }
# The server thread ahead of the workers (MinecraftServer.spin's Thread.setPriority).
horizon-threads = { path = "../horizon-threads" }
horizon-world = { path = "../horizon-world" }
horizon-protocol = { path = "../horizon-protocol" }
# server.properties, eula.txt and the user lists, shared with horizon-server.
horizon-server-files = { path = "../horizon-server-files" }
horizon-friends = { path = "../horizon-friends" }
# Horizon mods: sandboxed WebAssembly components, off unless installed and turned on (docs/mods.md).
horizon-mods = { path = "../horizon-mods" }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl"] }
winit = "0.30.13"
pollster = "0.4"
@@ -28,54 +22,29 @@ png = "0.18"
sha2 = "0.10.9"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
# Shader packs: pack.toml, and WGSL checked before it reaches wgpu.
toml = "1.1"
naga = { version = "30.0.1", features = ["wgsl-in"] }
# Java (OptiFine/Iris) shader packs, translated to WGSL.
horizon-shaderpack = { path = "../horizon-shaderpack" }
# Sound (ALSA on Linux); the `audio` feature.
cpal = { version = "0.16", optional = true }
cpal = "0.16"
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
miniz_oxide = "0.8"
ab_glyph = "0.2.32"
log = "0.4"
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
flate2 = { version = "1.1.10", default-features = false, features = ["rust_backend"] }
horizon-theme = { path = "../horizon-theme" }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md); `live` is
# the anonymous "I'm playing" ping the game sends itself (`heartbeat.rs`).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
# Opt-in data collection: the game keeps, the launcher sends (docs/telemetry.md).
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
horizon-bench = { path = "../horizon-bench" }
arboard = { version = "3.6.1", default-features = false, features = ["wayland-data-control"] }
zip = { version = "2", default-features = false, features = ["deflate"] }
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input); the `controllers` feature.
gilrs = { version = "0.11", optional = true }
# The DualSense's rumble, adaptive triggers and lights over HID (gilrs only reads it).
horizon-dualsense = { path = "../horizon-dualsense", optional = true }
# Controllers (evdev + libudev on Linux, IOKit on macOS, Windows.Gaming.Input).
gilrs = "0.11"
# HTTPS to the Minecraft services (online-mode session join).
ureq = { version = "2", default-features = false, features = ["tls"] }
[target.'cfg(unix)'.dependencies]
# The server thread's CPU time per tick (`HORIZON_SPIKES`, spans.rs).
rustix = { version = "1", default-features = false, features = ["std", "time"] }
[target.'cfg(windows)'.dependencies]
# DirectX 12, where Vulkan has no adapter or device, or by choice (`graphicsApi`, graphics.rs).
# Shaders compile with FXC (d3dcompiler_47.dll, part of Windows), or DXC when dxcompiler.dll is
# on the PATH.
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
[target.'cfg(windows)'.build-dependencies]
# The icon resource in the .exe (build.rs).
winresource = "0.1"
[features]
default = ["audio", "controllers"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
# Linux: the server build (`--no-default-features`, released as horizon-engine; docs/server.md).
audio = ["dep:cpal"]
# Controllers: gilrs, and the DualSense's own output. Without it only `horizon-ctl pad`'s virtual pad.
controllers = ["dep:gilrs", "dep:horizon-dualsense"]
# Records the call sites of mob random draws (`mob_ai_probe` traces).
rng-trace = ["horizon-core/rng-trace"]
@@ -83,5 +52,5 @@ rng-trace = ["horizon-core/rng-trace"]
workspace = true
[dev-dependencies]
# Every WGSL shader translated as wgpu's DirectX 12 backend does (gfx.rs tests).
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
# Parses and validates the composed entity shader (already built for wgpu).
naga = { version = "30.0.1", features = ["wgsl-in"] }
@@ -131,7 +131,6 @@ fn main() {
let tick = TickContext {
player_underwater: ambience.was_underwater(),
player_removed: removed,
..TickContext::default()
};
engine.tick(false, &tick, &mut backend);
}
+7 -59
View File
@@ -4,8 +4,7 @@
use horizon_client::{
Result,
arrows::{
self, ArrowEvent, ArrowParticle, ArrowScene, ArrowSound, BlockReaction, EntityHit, Pickup,
Touch,
self, ArrowEvent, ArrowParticle, ArrowScene, ArrowSound, BlockReaction, Pickup, Touch,
},
assets::Assets,
blocks::BlockFacts,
@@ -13,11 +12,11 @@ use horizon_client::{
items::ItemStack,
mobs::Who,
motion::{LAVA, MotionResources, WATER},
projectiles::{Hit, Projectile, ProjectileKind},
projectiles::{Projectile, ProjectileKind},
world::World,
};
use horizon_core::{
geometry::{Aabb, Vec3},
geometry::Vec3,
random::{LegacyRandom, RandomSource},
};
use serde_json::Value;
@@ -58,12 +57,11 @@ fn main() -> Result<()> {
let root = PathBuf::from(&args[0]);
let dir = PathBuf::from(&args[1]);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let mut scene = World::test_scene(&assets, &render, 1)?;
let mut scene = World::test_scene(&assets, 1)?;
fs::write(
dir.join("scene.json"),
serde_json::to_vec(
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":render.model_gallery}),
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":assets.model_gallery}),
)?,
)?;
if args[2] == "--describe" {
@@ -91,16 +89,11 @@ fn main() -> Result<()> {
&ItemStack::new("minecraft:arrow", 1),
f["intangible"].as_bool().unwrap(),
));
// The entity in the way: its box and whether it takes the damage.
let mut target: Option<(Aabb, bool)> = None;
for op in f["ops"].as_array().unwrap() {
let mut events = Vec::new();
let mut touched = "-".to_string();
match op["mode"].as_str().unwrap() {
"tick" => {
// The game's part of an entity hit: the damage it deals
// (recorded after the tick's own events).
let mut hurt = None;
if !arrow.arrow.as_ref().unwrap().removed {
let no_rain = |_| false;
let view = ArrowScene {
@@ -113,35 +106,9 @@ fn main() -> Result<()> {
mob_griefing: true,
tnt_explodes: true,
};
let targets: Vec<_> = target.iter().map(|t| (Who::Mob(2), t.0)).collect();
let (_, hit) = arrows::tick(&mut arrow, &view, &targets);
if let (Hit::Entity { .. }, Some((_, takes))) = (hit, target) {
let mut st = arrow.arrow.take().unwrap();
let damage = arrow.arrow_damage(&mut st.random);
hurt = Some((st.events.len(), damage));
arrow.arrow = Some(st);
let outcome = if takes {
EntityHit::Hurt
} else {
EntityHit::Refused
};
arrows::finish_entity_hit(&mut arrow, &view, outcome);
}
arrows::tick(&mut arrow, &view, &[]);
}
let a = arrow.arrow.as_mut().unwrap();
let (drop_at, dropped) = (arrow.position, a.pickup_item.count);
let raised = std::mem::take(&mut a.events);
let count = raised.len();
for (i, event) in raised.into_iter().map(Some).chain([None]).enumerate() {
if let Some((at, damage)) = hurt
&& at == i
{
events.push(format!("hurt {:08x}", damage.to_bits()));
}
let Some(event) = event else {
debug_assert_eq!(i, count);
break;
};
for event in arrow.arrow.as_mut().unwrap().events.drain(..) {
match event {
ArrowEvent::Particle {
kind,
@@ -224,28 +191,9 @@ fn main() -> Result<()> {
},
ArrowEvent::SplashEvent => events.push("splash-event".into()),
ArrowEvent::ShootEvent => events.push("shoot-event".into()),
ArrowEvent::Drop => events.push(format!(
"drop {} {dropped}",
bits(Vec3::new(
drop_at.x,
drop_at.y + f64::from(0.1f32),
drop_at.z
))
)),
}
}
}
"target" => {
// EntityDimensions.scalable(0.6, 1.8).makeBoundingBox.
let at = vector(&op["position"]);
let w = f64::from(0.6f32) / 2.0;
let h = f64::from(1.8f32);
let b = Aabb::new(
Vec3::new(at.x - w, at.y, at.z - w),
Vec3::new(at.x + w, at.y + h, at.z + w),
);
target = Some((b, op["takes"].as_bool().unwrap()));
}
"block" => {
let p = &op["position"];
let mut state = assets
@@ -31,14 +31,13 @@ fn main() -> Result<()> {
};
let root = PathBuf::from(root);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let mut scene = World::test_scene(&assets, &render, 1)?;
let mut scene = World::test_scene(&assets, 1)?;
let binding = collisions::load(&root, assets.registry)?;
let resources = MotionResources::load(&root, &assets)?;
fs::write(
scene_json,
serde_json::to_vec(&serde_json::json!({
"radius": 1, "resident_chunks": scene.chunks.len(), "model_gallery": render.model_gallery,
"radius": 1, "resident_chunks": scene.chunks.len(), "model_gallery": assets.model_gallery,
}))?,
)?;
let mut out = BufWriter::new(fs::File::create(output)?);
@@ -101,7 +100,6 @@ fn main() -> Result<()> {
player.auto_jump_enabled = p["enabled"].as_bool().unwrap();
player.auto_jump_time = p["time"].as_i64().unwrap() as i32;
MotionWorld {
entities: &[],
player: &mut player,
scene: &mut scene,
assets: &assets,
@@ -1,99 +0,0 @@
//! The Rust side of tools/test_block_entity_tickers_parity.py: the ops of
//! `VanillaBlockEntityTickerOracle` on `BlockEntities`, as `World` drives it
//! (a state write calls `state_changed`), printing the tick order.
use horizon_client::{
block_entities::{BlockEntities, Pos, Ticking},
containers::BlockEntity,
};
use std::collections::HashMap;
/// State ids of the fixture: 0 air, 1 a block whose block entity ticks, 2 a
/// jukebox without a record, 3 a block entity that never ticks (a chest).
fn state(name: &str) -> u32 {
match name {
"chest" | "barrel" => 3,
"jukebox" => 2,
_ => 1,
}
}
fn entity(name: &str) -> BlockEntity {
if name.starts_with("jukebox") {
// The game makes a jukebox's block entity when a record goes in.
return BlockEntity::Jukebox(Default::default());
}
BlockEntity::for_block(&format!("minecraft:{name}"))
.unwrap_or_else(|| panic!("no block entity for {name}"))
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let input = std::fs::read_to_string(&args[1]).expect("input");
let mut out = String::new();
let mut entities = BlockEntities::default();
let mut states: HashMap<Pos, u32> = HashMap::new();
let mut kinds: HashMap<Pos, String> = HashMap::new();
let table = vec![Ticking::No, Ticking::Yes, Ticking::Idle, Ticking::No];
for line in input.lines() {
let w: Vec<&str> = line.split_whitespace().collect();
let pos = |i: usize| -> Pos {
let n = |j: usize| w[i + j].parse::<i32>().expect("coordinate");
(n(0), n(1), n(2))
};
match w[0] {
"reset" => {
entities = BlockEntities::default();
entities.set_states(table.clone());
states.clear();
kinds.clear();
}
"load" => {
let mut chunk = Vec::new();
for i in (3..w.len()).step_by(4) {
let p = pos(i + 1);
states.insert(p, state(w[i]));
kinds.insert(p, w[i].to_owned());
chunk.push((p, entity(w[i])));
}
entities.load_chunk(chunk);
}
"place" => {
let p = pos(2);
// World::set, then the game adds the block entity.
let old = states.insert(p, state(w[1])).unwrap_or(0);
kinds.insert(p, w[1].to_owned());
entities.state_changed(p, old, state(w[1]));
entities.insert(p, entity(w[1]));
}
"replace" => {
let p = pos(1);
entities.insert(p, entity(&kinds[&p]));
}
"remove" => {
let p = pos(1);
entities.remove(&p);
let old = states.insert(p, 0).unwrap_or(0);
entities.state_changed(p, old, 0);
}
"record" => {
let p = pos(1);
let s = if w[4] == "1" { 1 } else { 2 };
let old = states.insert(p, s).unwrap_or(0);
entities.state_changed(p, old, s);
}
"unload" => {
let c = (w[1].parse::<i32>().unwrap(), w[2].parse::<i32>().unwrap());
entities.retain(|p, _| (p.0 >> 4, p.2 >> 4) != c);
}
"tick" => {
out.push_str("tick");
for (p, _) in entities.tickers(|p| states.get(&p).copied().unwrap_or(0)) {
out.push_str(&format!(" {},{},{}", p.0, p.1, p.2));
}
out.push('\n');
}
other => panic!("unknown op {other}"),
}
}
std::fs::write(&args[2], out).expect("output");
}
@@ -1,104 +0,0 @@
//! Full production fact loading, including file reads, interning and temporary destruction.
//! cargo run --profile fast -p horizon-client --example block_facts_bench -- ROOT [SAMPLES]
//! Use SAMPLES=1 with an OS peak-RSS wrapper; `digest` checks all facts
//! and the shape sharing partition (including sharing between outlines and edges).
use horizon_client::blocks::BlockFacts;
use horizon_world::{DATA_VERSION, nbt::Tag, registry::BlockRegistry};
use serde_json::Value;
use std::{
collections::{HashMap, hash_map::DefaultHasher},
fs,
hash::{Hash, Hasher},
hint::black_box,
path::Path,
time::Instant,
};
type Result<T> = horizon_client::Result<T>;
fn tag(value: &Value) -> Result<Tag> {
Ok(match value {
Value::String(s) => Tag::String(s.as_str().into()),
Value::Number(n) => Tag::Int(i32::try_from(n.as_i64().ok_or("registry number")?)?),
Value::Array(a) => Tag::List(a.iter().map(tag).collect::<Result<_>>()?),
Value::Object(o) => Tag::Compound(
o.iter()
.map(|(k, v)| Ok((k.as_str().into(), tag(v)?)))
.collect::<Result<_>>()?,
),
_ => return Err("registry value".into()),
})
}
fn registry(root: &Path) -> Result<BlockRegistry> {
let facts: Value = serde_json::from_slice(&fs::read(root.join("facts.json"))?)?;
Ok(BlockRegistry::from_nbt(&Tag::compound([
("DataVersion", Tag::Int(DATA_VERSION)),
(
"states",
Tag::List(
facts["states"]
.as_array()
.ok_or("registry states")?
.iter()
.map(|s| tag(&s[4]))
.collect::<Result<_>>()?,
),
),
("defaults", tag(&facts["defaults"])?),
]))?)
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
let root = Path::new(
args.first()
.ok_or("usage: block_facts_bench ROOT [SAMPLES|digest]")?,
);
let setup = Instant::now();
let registry = registry(root)?;
println!(
"registry_setup_ms={:.3}",
setup.elapsed().as_secs_f64() * 1000.0
);
if args.get(1).is_some_and(|s| s == "digest") {
let facts = BlockFacts::load(root, &registry)?;
let mut hash = DefaultHasher::new();
let mut shapes = HashMap::new();
facts.air.hash(&mut hash);
for state in &facts.states {
format!("{state:?}").hash(&mut hash);
for shape in [&state.outline, &state.outline_edges] {
let next = shapes.len();
shapes
.entry((shape.as_ptr(), shape.len()))
.or_insert(next)
.hash(&mut hash);
}
}
println!(
"states={} shared_shapes={} digest={:016x}",
facts.states.len(),
shapes.len(),
hash.finish()
);
return Ok(());
}
let samples: usize = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
assert!(samples > 0);
// First load is reported separately; OS file caches are not forcibly evicted.
let load = || -> Result<f64> {
let start = Instant::now();
let facts = black_box(BlockFacts::load(black_box(root), black_box(&registry))?);
let ms = start.elapsed().as_secs_f64() * 1000.0;
drop(facts);
Ok(ms)
};
println!("first_load_ms={:.3}", load()?);
if samples > 1 {
load()?;
load()?;
let mut times = (0..samples).map(|_| load()).collect::<Result<Vec<_>>>()?;
println!("samples_ms={times:.3?}");
times.sort_by(f64::total_cmp);
println!("median_ms={:.3}", times[times.len() / 2]);
}
Ok(())
}
@@ -12,11 +12,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let root = Path::new(&args[1]);
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
let tables = MeshTables::new(
&assets,
&render,
&facts,
models::load_visibility(root, assets.registry)?,
)?;
@@ -14,17 +14,13 @@ use std::{
io::{BufRead, BufReader, BufWriter, Write},
path::Path,
};
/// The stage sprite origin the cracks carry (the oracle compares the geometry and UVs only).
const STAGE: [f32; 2] = [0.25, 0.5];
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let root = Path::new(&args[1]);
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
let tables = MeshTables::new(
&assets,
&render,
&facts,
models::load_visibility(root, assets.registry)?,
)?;
@@ -37,7 +33,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut vertices = Vec::new();
let mut translucent = false;
if facts.get(state).render_shape == RenderShape::Model {
let models = render
let models = assets
.model_catalog
.collect(state, BlockPos::new(p[0], p[1], p[2]))?;
let offset = tables.offset(state, p[0], p[2]).unwrap_or([0.0; 3]);
@@ -49,7 +45,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.iter()
.any(|(_, m)| m.faces.iter().any(|f| f.translucent));
for (_, m) in models {
breaking::geometry(m, origin, translucent, STAGE, &mut vertices);
breaking::geometry(m, origin, translucent, &mut vertices);
}
}
let quads: Vec<_> = vertices
@@ -60,7 +56,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
q.iter()
.map(|v| {
assert_eq!(v.color, 0xffff_ffff);
assert_eq!(v.light, breaking::stage_light(STAGE));
assert_eq!(v.light, 240 | (240 << 8));
assert_eq!(v.layer, if translucent { 12 } else { 11 });
[v.pos[0], v.pos[1], v.pos[2], v.uv[0], v.uv[1]]
})
@@ -82,19 +82,12 @@ fn main() -> horizon_client::Result<()> {
let brewing = horizon_client::brewing::Brewing::load(root, &items);
// The inventory menu never reads banner patterns.
let banners = horizon_client::banner::Banners::default();
let station_recipes = horizon_client::stations::StationRecipes::default();
let enchantments = horizon_client::enchant::Enchantments::default();
let names = horizon_client::creative::Names::default();
let env = Env {
items: &items,
recipes: &recipes,
brewing: &brewing,
banners: &banners,
station_recipes: &station_recipes,
enchantments: &enchantments,
names: &names,
trade: None,
maps: None,
};
let mut out = String::new();
for line in input.lines() {
@@ -31,12 +31,10 @@ fn main() -> Result<()> {
};
let (seed, size) = (flag("--seed", 12345), flag("--size", 8) as i32);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = MeshTables::new(
&assets,
&render,
&facts,
models::load_visibility(&root, assets.registry)?,
)?;
@@ -134,7 +132,6 @@ fn main() -> Result<()> {
let mut mesher = Mesher {
nether_shading: false,
assets: &assets,
render: &render,
tables: &tables,
biomes: &biomes,
visibility: tables.visibility.clone(),
@@ -142,7 +139,6 @@ fn main() -> Result<()> {
blend_radius: 2,
smooth_lighting: true,
cutout_leaves: true,
cull_leaves: false,
};
let mut best = f64::MAX;
let mut quads = 0usize;
@@ -135,7 +135,7 @@ fn tree(d: &Dispatcher, node: usize) -> Value {
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("tree") {
let d = &vanilla::registered(false);
let d = vanilla::release_dispatcher();
let mut v = tree(d, 0);
v["type"] = json!("root");
fs::write(&args[2], v.to_string())?;
@@ -30,11 +30,7 @@ fn main() -> horizon_client::Result<()> {
),
};
let info = biomes.get(biomes.id(&format!("minecraft:{}", f[4])));
let mut sky = environment.sample(
f[1].parse()?,
info.sky.map(horizon_client::biome::rgb),
info.fog.map(horizon_client::biome::rgb),
);
let mut sky = environment.sample(f[1].parse()?, info.sky, info.fog);
if dimension == "overworld" {
apply_weather(&mut sky, f[2].parse()?, f[3].parse()?);
}
@@ -1,30 +0,0 @@
//! Firework tooltip companion for tools/test_firework_tooltip_parity.py: the
//! lines `item_tooltip::firework_lines` gives each stack (SNBT, one per line;
//! the `#resources` line names the prepared resources for en_us).
use horizon_client::{enchant::Enchantments, item_data::parse_stack, item_tooltip::firework_lines};
use std::{error::Error, fmt::Write, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = std::env::args().collect();
let input = std::fs::read_to_string(&args[1])?;
let root = input
.lines()
.find_map(|l| l.strip_prefix("#resources "))
.map(PathBuf::from)
.ok_or("no #resources line")?;
// Use the inventory's filtered translations so missing tooltip keys are caught.
let enchantments = Enchantments::load(&root);
let translate = |key: &str| enchantments.translate(key).map(str::to_owned);
let mut output = String::new();
for line in input.lines().filter(|l| !l.starts_with('#')) {
let stack = parse_stack(line)?.ok_or("empty stack")?;
let lines = firework_lines(&stack, &translate);
if lines.is_empty() {
writeln!(output, "-")?;
} else {
writeln!(output, "{}", lines.join(" | "))?;
}
}
std::fs::write(&args[2], output)?;
Ok(())
}
@@ -233,32 +233,22 @@ fn mouse(c: &Value, out: &mut impl Write) -> Result<()> {
fn scroll(c: &Value, out: &mut impl Write) -> Result<()> {
let mut handler = MouseLook::default();
let mut slot = c["slot"].as_i64().unwrap() as i32;
let spectator = c["spectator"].as_bool().unwrap_or(false);
let mut speed = 0.05;
// OptionInstance.set quantises the wheel sensitivity.
let mut file = OptionsFile::default();
file.set("mouseWheelSensitivity", V::Double(db(&c["sens"])));
let sensitivity = file.double("mouseWheelSensitivity");
writeln!(out, "scroll")?;
for op in c["ops"].as_array().unwrap() {
if let Some((wheel, (_, y))) =
if let Some((wheel, _)) =
handler.on_scroll(db(&op[0]), db(&op[1]), b(&c["discrete"]), sensitivity)
{
if spectator {
speed = horizon_client::mouse::spectator_flying_speed(speed, y);
} else {
slot = horizon_client::mouse::ScrollWheelHandler::next_selection(
f64::from(wheel),
slot,
9,
);
}
}
if spectator {
writeln!(out, "{slot} {}", fb(speed))?;
} else {
writeln!(out, "{slot}")?;
slot = horizon_client::mouse::ScrollWheelHandler::next_selection(
f64::from(wheel),
slot,
9,
);
}
writeln!(out, "{slot}")?;
}
Ok(())
}
@@ -1,33 +0,0 @@
//! Profile the two mandatory passes over gameplay-facts.json independently.
//! Build with `cargo build --release -p horizon-client --example gameplay_decode_profile`.
//! Each invocation runs one phase in a separate process for CPU/RSS measurements.
#[allow(dead_code)]
#[path = "../src/blocks/decode.rs"]
mod decode;
use std::{hint::black_box, path::Path, time::Instant};
// The included private decoder expects the parent crate Result alias.
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let mut args = std::env::args().skip(1);
let root = args.next().ok_or("expected resource directory")?;
let mode = args.next().ok_or("expected validate or typed")?;
if mode != "validate" && mode != "typed" {
return Err("expected validate or typed".into());
}
let total = Instant::now();
let bytes = std::fs::read(Path::new(&root).join("gameplay-facts.json"))?;
let started = Instant::now();
if mode == "validate" {
let parsed: decode::Validate = serde_json::from_slice(&bytes)?;
black_box(parsed);
} else {
let parsed: decode::Gameplay<'_> = serde_json::from_slice(&bytes)?;
black_box(parsed);
}
println!("phase_ms={:.3}", started.elapsed().as_secs_f64() * 1000.0);
println!("total_ms={:.3}", total.elapsed().as_secs_f64() * 1000.0);
Ok(())
}
@@ -1,38 +0,0 @@
//! Selected-slot drop state for the vanilla key-handler parity harness.
use horizon_client::command::args::GameMode;
use horizon_client::items::{ItemStack, take_held_drop};
fn main() {
for mode in [
GameMode::Survival,
GameMode::Creative,
GameMode::Adventure,
GameMode::Spectator,
] {
for all in [false, true] {
for _selected in [0, 8] {
for count in [0, 1, 4, 64] {
let mut slot = Some(ItemStack::new("minecraft:stone", count));
let mut dropped = 0;
let mut swings = 0;
let mut packets = 0;
for _ in 0..2 {
if let Some(stack) =
take_held_drop(&mut slot, all, mode == GameMode::Spectator)
{
dropped += stack.count;
swings += 1;
}
if mode != GameMode::Spectator {
packets += 1;
}
println!(
"{} {dropped} {swings} {packets}",
slot.as_ref().map_or(0, |s| s.count),
);
}
}
}
}
}
}
@@ -1,122 +0,0 @@
use horizon_client::interaction_input::{
Button, InteractionInput, InteractionWorld, TargetUseWorld, use_on_target,
};
use serde_json::Value;
use std::io::{BufRead, Write};
struct World {
using: bool,
destroying: bool,
entity: bool,
dig_on_attack: bool,
begin_using: bool,
delay: u32,
events: Vec<String>,
passing: bool,
}
impl InteractionWorld for World {
fn using_item(&self) -> bool {
self.using
}
fn release_using(&mut self) {
self.events.push("release".into());
self.using = false;
}
fn start_attack(&mut self) -> bool {
self.events.push("attack".into());
if !self.entity {
self.destroying = self.dig_on_attack;
}
false
}
fn start_use(&mut self) {
if self.destroying {
return;
}
self.delay = 4;
let entity = self.entity;
use_on_target(self, entity);
}
fn pick(&mut self) {
self.events.push("pick".into());
}
fn use_delay(&self) -> u32 {
self.delay
}
fn continue_attack(&mut self, down: bool) {
if self.using {
return;
}
let down = down && !self.entity;
self.events.push(format!("continue {down}"));
if !down {
self.destroying = false;
}
}
}
impl TargetUseWorld for World {
fn use_entity(&mut self) -> bool {
self.events
.push(if self.passing { "entity" } else { "use" }.into());
if self.passing {
false
} else {
self.using = self.begin_using;
true
}
}
fn use_block(&mut self) -> bool {
self.events
.push(if self.passing { "block" } else { "use" }.into());
if self.passing {
false
} else {
self.using = self.begin_using;
true
}
}
fn use_item(&mut self) {
self.events.push("item".into());
self.using = self.begin_using;
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
for (i, line) in input.lines().enumerate() {
let row: Value = serde_json::from_str(&line?)?;
let flag = |key: &str| row[key].as_bool().unwrap();
let mut world = World {
using: flag("using"),
destroying: flag("destroying"),
entity: flag("entity"),
dig_on_attack: flag("dig_on_attack"),
begin_using: flag("begin_using"),
delay: row["delay"].as_u64().unwrap() as u32,
events: vec![],
passing: row["passing"].as_bool().unwrap_or(false),
};
let mut input = InteractionInput::default();
for button in row["clicks"].as_array().unwrap() {
input.press(match button.as_u64().unwrap() {
0 => Button::Attack,
1 => Button::Use,
_ => Button::Pick,
});
}
input.tick(&mut world, flag("attack_down"), flag("use_down"));
writeln!(out, "== {i}")?;
for event in world.events {
writeln!(out, "{event}")?;
}
writeln!(
out,
"state {} {} {}",
world.using, world.destroying, world.delay
)?;
}
Ok(())
}
+7 -10
View File
@@ -22,7 +22,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let port: u16 = args.next().ok_or("port")?.parse()?;
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let state = |name: &str| assets.registry.default_state(name).map_or(0, |s| s.0);
let state = |name: &str| {
assets
.registry
.default_state(name)
.map_or(0, |s| s.0 as u16)
};
let sync = Arc::new(SyncData::load(&root)?);
let biomes: Vec<String> = sync
.dynamic
@@ -83,7 +88,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
seed: 0,
max_players: 8,
dedicated: false,
integrated: false,
},
);
let mut next_entity = 100u32;
@@ -101,9 +105,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
sprinting: false,
equipment: Default::default(),
health: 20.0,
attributes: Vec::new(),
bed: None,
living_flags: 0,
};
loop {
world.game_time += 1;
@@ -119,8 +120,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
clock_rate: 1.0,
tick_rate: 20.0,
tick_frozen: false,
tick_steps: (0, 0),
collision: None,
};
for event in hub.tick(&mut ctx) {
match event {
@@ -132,9 +131,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("[lan] {packet:?}");
}
}
HostEvent::Chat { line, .. } | HostEvent::Warn(line) | HostEvent::Error(line) => {
println!("{line}")
}
HostEvent::Chat(line) => println!("{line}"),
HostEvent::Left(_) => {}
}
}
+5 -12
View File
@@ -206,7 +206,7 @@ fn incremental(
sky_light: bool,
edits: usize,
) {
use horizon_client::light::{LightChanges, relight};
use horizon_client::light::relight;
use std::sync::Arc;
let mut world: HashMap<(i32, i32), Arc<Chunk>> = HashMap::new();
let inner: Vec<(i32, i32)> = {
@@ -232,7 +232,7 @@ fn incremental(
c.lit = true;
world.insert((x, z), Arc::new(c));
}
let palette: Vec<u32> = [
let palette: Vec<u16> = [
"minecraft:air",
"minecraft:stone",
"minecraft:torch",
@@ -242,7 +242,7 @@ fn incremental(
"minecraft:glass",
]
.iter()
.filter_map(|n| registry.default_state(n).map(|s| s.0))
.filter_map(|n| registry.default_state(n).map(|s| s.0 as u16))
.chain(
facts
.states
@@ -257,7 +257,7 @@ fn incremental(
]
.contains(&&*f.name)
})
.map(|(i, _)| i as u32)
.map(|(i, _)| i as u16)
.step_by(3),
)
.collect();
@@ -285,14 +285,7 @@ fn incremental(
if old == state || !tables.differs(old, state) {
continue;
}
relight(
&mut world,
tables,
sky_light,
&[(x, y, z)],
false,
&mut LightChanges::default(),
);
relight(&mut world, tables, sky_light, &[(x, y, z)], false);
for dz in -1..=1 {
for dx in -1..=1 {
let (kx, kz) = (cx + dx, cz + dz);
@@ -1,153 +0,0 @@
//! Read-only saved chunk benchmark. All timings exclude resource/corpus setup.
//!
//! load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]
//! Add --profile-worker when a sampling collector cannot sample the main thread.
//! Reports warm filesystem SaveDir::load (read, inflate, NBT, decode and light),
//! plus isolated inflate/NBT and native decode. No files in the save are changed.
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
save::{SaveDir, SaveTables, decode_with},
};
use horizon_world::{nbt::Limits, region::RegionFile};
use sha2::{Digest, Sha256};
use std::{hint::black_box, path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
if std::env::args().any(|a| a == "--profile-worker") {
std::thread::spawn(|| run().map_err(|e| e.to_string()))
.join()
.expect("benchmark worker")
.map_err(Into::into)
} else {
run()
}
}
fn run() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let usage = "load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]";
let root = PathBuf::from(args.first().ok_or(usage)?);
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
let flag = |name: &str, default: usize| -> Result<usize> {
Ok(match args.iter().position(|a| a == name) {
Some(i) => args.get(i + 1).ok_or(usage)?.parse()?,
None => default,
})
};
let rounds = flag("--rounds", 5)?;
let limit = flag("--limit", 256)?;
if rounds == 0 || limit == 0 {
return Err("rounds and limit must be positive".into());
}
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
let source = SaveDir::new(dimension.clone());
let _ = source.tables.set(tables.clone());
let mut paths: Vec<_> = std::fs::read_dir(dimension.join("region"))?
.map(|e| e.map(|e| e.path()))
.collect::<std::io::Result<_>>()?;
paths.retain(|p| p.extension().is_some_and(|e| e == "mca"));
paths.sort();
let mut corpus = Vec::new();
'regions: for path in paths {
let mut region = RegionFile::open(path)?;
let mut positions: Vec<_> = region.chunks().map(|(pos, _)| pos).collect();
positions.sort_by_key(|p| (p.x, p.z));
for pos in positions {
let record = region.read(pos)?.ok_or("missing record")?;
let tag = record.nbt(Limits::default())?;
if decode_with(&tag, assets.registry, &biomes, Some(&tables)).is_none() {
continue;
}
corpus.push((pos, record, tag));
if corpus.len() == limit {
break 'regions;
}
}
}
if corpus.is_empty() {
return Err("no full chunks in corpus".into());
}
let mut records_hash = Sha256::new();
for (pos, record, _) in &corpus {
records_hash.update(pos.x.to_le_bytes());
records_hash.update(pos.z.to_le_bytes());
records_hash.update((record.compressed.len() as u64).to_le_bytes());
records_hash.update(&record.compressed);
}
println!(
"{}",
serde_json::json!({"corpus": dimension, "chunks": corpus.len(), "rounds": rounds,
"compressed_records_sha256": format!("{:x}", records_hash.finalize()),
"compressed_bytes": corpus.iter().map(|(_, r, _)| r.compressed.len()).sum::<usize>()})
);
// Prime SaveDir's region handles and the filesystem cache before measurement.
for (pos, _, _) in &corpus {
black_box(
source
.load(pos.x, pos.z, assets.registry, &biomes)
.ok_or("load failed")?,
);
}
for round in 0..rounds {
let started = Instant::now();
for (pos, _, _) in &corpus {
black_box(
source
.load(pos.x, pos.z, assets.registry, &biomes)
.ok_or("load failed")?,
);
}
let load_s = started.elapsed().as_secs_f64();
let started = Instant::now();
for (_, record, _) in &corpus {
black_box(record.nbt(Limits::default())?);
}
let nbt_s = started.elapsed().as_secs_f64();
let started = Instant::now();
for (_, _, tag) in &corpus {
black_box(
decode_with(tag, assets.registry, &biomes, Some(&tables)).ok_or("decode failed")?,
);
}
let decode_s = started.elapsed().as_secs_f64();
println!(
"{}",
serde_json::json!({"round": round, "load_s": load_s,
"inflate_nbt_s": nbt_s, "decode_s": decode_s})
);
}
if args.iter().any(|a| a == "--checksum") {
let mut hash = Sha256::new();
let mut light_loaded = 0;
for (pos, _, _) in &corpus {
let chunk = source
.load(pos.x, pos.z, assets.registry, &biomes)
.ok_or("load failed")?;
hash.update(pos.x.to_le_bytes());
hash.update(pos.z.to_le_bytes());
light_loaded += usize::from(chunk.light_loaded);
for section in &chunk.sections {
for i in 0..4096 {
hash.update(section.get(i).to_le_bytes());
hash.update([section.light(i)]);
}
for i in 0..64 {
hash.update(section.biomes.get(i).to_le_bytes());
}
}
}
println!(
"{}",
serde_json::json!({"blocks_biomes_light_sha256": format!("{:x}", hash.finalize()),
"light_loaded": light_loaded})
);
}
Ok(())
}
@@ -1,90 +0,0 @@
//! Probe for tools/test_marker_particles_parity.py.
use horizon_client::{
Result,
animate_tick::{AnimateContext, AnimateTables, animate_tick, marker_particle_target},
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
chunk::Chunk,
collisions,
command::args::GameMode,
items::ItemStack,
particles::{ParticleEngine, ParticleOptions, ParticleType},
world::World,
};
use horizon_core::random::RandomSource;
use serde_json::Value;
use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader, BufWriter, Write},
path::Path,
sync::Arc,
};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(".local/horizon-client/26.3");
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
let biomes = Biomes::load(root)?;
let collision = collisions::load(root, assets.registry)?;
let tables = AnimateTables::load(root, assets.registry, &biomes)?;
let mut world = World::new(facts.air, 0);
let barrier = assets
.registry
.default_state("minecraft:barrier")
.ok_or("barrier")?;
for cz in -2..2 {
for cx in -2..2 {
world
.chunks
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
for y in 33..96 {
for z in 0..16 {
for x in 0..16 {
world.set(cx * 16 + x, y, cz * 16 + z, barrier)?;
}
}
}
}
}
let mut engine = ParticleEngine::new(root, &HashMap::new());
let mut out = BufWriter::new(File::create(&args[2])?);
for line in BufReader::new(File::open(&args[1])?).lines() {
let case: Value = serde_json::from_str(&line?)?;
let mode = GameMode::ALL
.into_iter()
.find(|mode| Some(mode.name()) == case["mode"].as_str())
.ok_or("mode")?;
let stack = ItemStack::new(
&format!("minecraft:{}", case["main"].as_str().ok_or("main")?),
case["count"].as_u64().ok_or("count")? as u32,
);
let target = marker_particle_target(mode, Some(&stack));
writeln!(out, "{}", target.unwrap_or("none"))?;
// Exercise the real ambient sampling loop in a dry barrier-filled world.
let options =
ParticleOptions::block(ParticleType::BlockMarker, "item/barrier".into(), 0xFFFFFF);
let options = |_| Some(options.clone());
let ctx = AnimateContext {
world: &world,
facts: &facts,
collision: &collision,
biomes: &biomes,
registry: assets.registry,
raining_at: &|_| false,
marker: target.map(|name| (name, &options as &dyn Fn(_) -> _)),
};
engine.seed_randoms(1057, 0, 0);
engine.requested = Some(Vec::new());
animate_tick(&tables, &mut engine, &ctx, [0, 64, 0]);
for (options, p, v) in engine.requested.take().ok_or("requests")? {
assert_eq!(options.ty, ParticleType::BlockMarker);
assert_eq!(v, [0.0; 3]);
writeln!(out, "marker {} {} {}", p[0], p[1], p[2])?;
}
writeln!(out, "next {}", engine.random().next_int())?;
}
Ok(())
}
+8 -126
View File
@@ -11,67 +11,20 @@ use horizon_client::{
items::Items,
menu::{Env, Input, Kind, Menu},
};
use horizon_protocol::registry::Ids;
use serde_json::Value;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
fs,
path::Path,
};
/// The case's maps: (locked, scale) by id, and the next free id.
#[derive(Default)]
struct CaseMaps {
maps: RefCell<HashMap<i32, (bool, u8)>>,
next: Cell<i32>,
}
impl horizon_client::menu::Maps for CaseMaps {
fn state(&self, id: i32) -> Option<(bool, u8)> {
self.maps.borrow().get(&id).copied()
}
/// `MapItemSavedData.scaled` (unlocked, one scale up) or `locked`.
fn derive(&self, id: i32, scale: bool) -> Option<i32> {
let (_, s) = self.state(id)?;
let new = self.next.get();
self.next.set(new + 1);
let made = if scale {
(false, (s + 1).min(4))
} else {
(true, s)
};
self.maps.borrow_mut().insert(new, made);
Some(new)
}
}
use std::{fs, path::Path};
fn state(menu: &Menu) -> String {
let slots: Vec<String> = (0..menu.slot_count())
.map(|i| canon_stack(menu.slot(i)))
.collect();
let data: String = menu.data.iter().map(|d| format!(" {d}")).collect();
let equipped: Vec<String> = menu
.equipped
.iter()
.map(|(slot, stack)| format!("{slot}:{}", canon_stack(stack.as_ref())))
.collect();
let line = format!(
"{} | {} |{data} | {} | {}",
format!(
"{} | {} |{data} | {}",
slots.join(" "),
canon_stack(menu.carried.as_ref()),
menu.drops.join(";"),
equipped.join(";")
);
if menu.kind() != Kind::Lectern {
return line;
}
// No inventory slots: the inventory Take Book fills.
let items: String = (0..36)
.map(|i| format!(" {}", canon_stack(menu.player_item(i))))
.collect();
format!("{line} | inv{items}")
menu.drops.join(";")
)
}
fn main() -> horizon_client::Result<()> {
@@ -85,28 +38,13 @@ fn main() -> horizon_client::Result<()> {
let recipes = Recipes::load(root, &items)?;
let brewing = horizon_client::brewing::Brewing::load(root, &items);
let banners = horizon_client::banner::Banners::load(root);
let station_recipes = horizon_client::stations::StationRecipes::load(root, &items);
let enchantments = horizon_client::enchant::Enchantments::load(root);
let names = horizon_client::creative::Names::load(root);
let case_maps = CaseMaps::default();
let env = Env {
items: &items,
recipes: &recipes,
brewing: &brewing,
banners: &banners,
station_recipes: &station_recipes,
enchantments: &enchantments,
names: &names,
trade: None,
maps: Some(&case_maps),
};
// The mob effects' network ids (BeaconMenu.encodeEffect).
let builtin = horizon_protocol::registry::Builtin::find(root)
.and_then(|p| horizon_protocol::registry::Builtin::load(&p).ok());
let effects = builtin
.as_ref()
.and_then(|b| b.get("minecraft:mob_effect"))
.cloned();
let mut out = String::new();
for line in input.lines() {
let case: Value = serde_json::from_str(line)?;
@@ -124,41 +62,15 @@ fn main() -> horizon_client::Result<()> {
.unwrap_or(name),
)
.ok_or("menu kind")?;
// [id, scale, locked] each; the level's new maps from 1000.
case_maps.next.set(1000);
*case_maps.maps.borrow_mut() = case["maps"]
.as_array()
.into_iter()
.flatten()
.map(|m| {
let id = m[0].as_i64().unwrap_or(0) as i32;
(
id,
(
m[2].as_bool().unwrap_or(false),
m[1].as_u64().unwrap_or(0) as u8,
),
)
})
.collect();
let mut menu = Menu::new(kind);
menu.creative = case["creative"].as_bool().unwrap_or(false);
menu.levels = case["levels"].as_i64().unwrap_or(0) as i32;
for (i, slot) in case["slots"].as_array().ok_or("slots")?.iter().enumerate() {
menu.set_slot(&env, i, parse_stack(slot.as_str().unwrap_or("-"))?);
}
menu.carried = parse_stack(case["carried"].as_str().unwrap_or("-"))?;
for (i, item) in case["inventory"]
.as_array()
.into_iter()
.flatten()
.enumerate()
{
menu.set_player_item(i, parse_stack(item.as_str().unwrap_or("-"))?);
}
menu.settle(&env);
for (i, v) in case["data"].as_array().ok_or("data")?.iter().enumerate() {
menu.set_data(i, v.as_i64().unwrap_or(0) as i32);
menu.data[i] = v.as_i64().unwrap_or(0) as i32;
}
let mut furnace = furnace_kind.map(|kind| Furnace {
kind,
@@ -170,7 +82,6 @@ fn main() -> horizon_client::Result<()> {
speed: 1.0,
});
menu.drops.clear();
menu.equipped.clear();
out += &format!("init {}\n", state(&menu));
for op in case["ops"].as_array().ok_or("ops")? {
let slot = op[0].as_i64().unwrap_or(0) as i32;
@@ -182,26 +93,6 @@ fn main() -> horizon_client::Result<()> {
out += &format!("{}\n", state(&menu));
continue;
}
// ServerboundRenameItemPacket: AnvilMenu.setItemName.
"RENAME" => {
menu.rename(&env, op[3].as_str().unwrap_or_default());
out += &format!("{}\n", state(&menu));
continue;
}
// ServerboundSetBeaconPacket: BeaconMenu.updateEffects.
"SET_BEACON" => {
fn power<'a>(ids: Option<&Ids>, v: &'a Value) -> Option<(i32, &'a str)> {
let name = v.as_str()?;
Some((ids?.id(name)?, name))
}
let (primary, secondary) = (
power(effects.as_ref(), &op[3]),
power(effects.as_ref(), &op[4]),
);
menu.beacon_update(&env, primary, secondary);
out += &format!("{}\n", state(&menu));
continue;
}
// ServerboundContainerSlotStateChangedPacket.
"SLOT_STATE" => {
if (0..9).contains(&slot) {
@@ -212,17 +103,8 @@ fn main() -> horizon_client::Result<()> {
}
_ => {}
}
let clicked = if op[2] == "QUICK_MOVE_ALL" {
// A Shift double click: lastQuickMoved is the op's fourth field.
let moved = parse_stack(op[3].as_str().unwrap_or("-"))?;
match moved {
Some(moved) => menu.quick_move_all(&env, slot as usize, &moved),
None => Ok(()),
}
} else {
let kind = Input::parse(op[2].as_str().unwrap_or_default()).ok_or("input")?;
menu.click(&env, slot, button, kind)
};
let kind = Input::parse(op[2].as_str().unwrap_or_default()).ok_or("input")?;
let clicked = menu.click(&env, slot, button, kind);
if let Some(f) = furnace.as_mut() {
let before = f.items[0].clone();
f.items = [0, 1, 2].map(|i| menu.slot(i).cloned());
@@ -31,7 +31,14 @@ fn main() -> horizon_client::Result<()> {
};
let s = sprites.mips[layer as usize];
writeln!(out, "== {name}")?;
for mip in horizon_client::mipmap::generate(base, size.trailing_zeros(), s) {
for mip in horizon_client::mipmap::generate(
base,
size.trailing_zeros(),
s.strategy,
s.alpha_cutoff_bias,
s.has_transparent,
s.item,
) {
write!(out, "{}x{}", mip.width, mip.height)?;
for row in mip.pixels.chunks(mip.width as usize) {
out.push(' ');
+6 -20
View File
@@ -19,7 +19,7 @@ use horizon_client::{
},
mobs::{
Mob, MobData, MobEvent, MobTypes, MobWorld, PlayerView, Seen, Species, Who,
control::Operation, seen_grid::SeenGrid, spawning::LocalDifficulty,
control::Operation, spawning::LocalDifficulty,
},
world::World,
};
@@ -902,10 +902,6 @@ fn main() -> horizon_client::Result<()> {
for flag in &spec.flags {
match flag.as_str() {
"persistent" => m.persistent = true,
"ridden" => {
m.extra.head = Some("minecraft:red_harness".into());
m.rider = Some(horizon_client::mobs::equine::Rider::default());
}
"noai" => m.no_ai = true,
// Warden.finalizeSpawn's own part.
"digcooldown" | "emerge" => horizon_client::mobs::warden::finalize(
@@ -999,7 +995,6 @@ fn main() -> horizon_client::Result<()> {
hero: false,
uuid: PLAYER_UUID,
allies: Default::default(),
remote: false,
}
});
let mut scratch = LegacyRandom::new(0);
@@ -1012,9 +1007,6 @@ fn main() -> horizon_client::Result<()> {
// The MobWorld every step sees, over a snapshot of the mobs.
macro_rules! mob_world {
($snapshot:expr) => {
mob_world!($snapshot, None)
};
($snapshot:expr, $grid:expr) => {
MobWorld {
teams: Default::default(),
world: &world,
@@ -1024,10 +1016,7 @@ fn main() -> horizon_client::Result<()> {
collision: &collision,
data: &data,
player: player.clone(),
nearest_player: player.as_ref().filter(|p| !p.spectator).map(|p| p.position),
others: $snapshot,
others_grid: $grid,
others_index: None,
daylight: (world.day_time.rem_euclid(24000)) < 12000,
game_time: world.game_time,
day_time: world.day_time,
@@ -1072,8 +1061,6 @@ fn main() -> horizon_client::Result<()> {
p.yaw = yaw;
}
let mut snapshot: Vec<Seen> = mobs.iter().filter(|m| !m.removed).map(seen).collect();
// The snapshot by position, kept as the game keeps it.
let mut grid = SeenGrid::new(snapshot.iter().map(|e| e.bounds));
let w = mob_world!(&snapshot);
let mut events = Vec::new();
let mut heart_events = Vec::new();
@@ -1133,13 +1120,13 @@ fn main() -> horizon_client::Result<()> {
}
#[allow(unused_mut)] // filled only with the rng-trace feature
let mut traces: Vec<String> = Vec::new();
let mut pushes = horizon_client::mobs::PendingPushes::default();
let mut pushes = Vec::new();
let mut born = Vec::new();
for i in 0..mobs.len() {
if mobs[i].removed {
continue;
}
let w = mob_world!(&snapshot, Some(&grid));
let w = mob_world!(&snapshot);
let m = &mut mobs[i];
horizon_client::mobs::apply_mob_pushes(m, &mut pushes);
let from = events.len();
@@ -1255,10 +1242,9 @@ fn main() -> horizon_client::Result<()> {
}
// ServerLevel ticks entities one after another: the ones
// after this mob see it as it is now.
if let Some(k) = snapshot.iter().position(|e| e.id == mobs[i].id) {
snapshot[k] = seen(&mobs[i]);
snapshot[k].alive &= !mobs[i].removed;
grid.update(k, snapshot[k].bounds);
if let Some(e) = snapshot.iter_mut().find(|e| e.id == mobs[i].id) {
*e = seen(&mobs[i]);
e.alive &= !mobs[i].removed;
}
}
for m in &mut mobs {
@@ -1,273 +0,0 @@
//! The Horizon side of a mod port's harness (tools/mod_port_harness.py,
//! docs/modpacks.md): builds the content root for carried-over mods and
//! writes what Horizon makes of every block state, drop, recipe and tag the
//! NeoForge oracle exported.
//!
//! cargo run -p horizon-client --example mod_port_probe -- PREPARED WORK OUT.json [MOD...]
//!
//! WORK is a `horizon-port-pack` work folder: `oracle/facts.json` and
//! `mods/<id>/carry/`. The root is built in `WORK/horizon-root`.
use horizon_client::{
Result,
assets::Assets,
blocks::BlockFacts,
crafting::{Recipe, Recipes},
items::{ItemStack, Items},
mod_content::{self, ContentMod},
stations::StationRecipes,
};
use horizon_core::{geometry::Axis, random::LegacyRandom};
use horizon_world::chunk::BlockState;
use serde_json::{Map, Value, json};
use std::{collections::BTreeMap, fs, path::Path};
fn boxes(list: impl IntoIterator<Item = [f64; 6]>) -> Value {
let mut v: Vec<[f64; 6]> = list.into_iter().collect();
v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
json!(v)
}
fn sound(s: &horizon_client::blocks::Sounds) -> Value {
json!({"volume": s.volume, "pitch": s.pitch, "break": &*s.break_sound, "step": &*s.step,
"place": &*s.place, "hit": &*s.hit, "fall": &*s.fall})
}
fn tool(name: &str) -> Option<ItemStack> {
match name {
"hand" => None,
"silk_touch_pickaxe" => {
let mut pick = ItemStack::new("minecraft:diamond_pickaxe", 1);
pick.enchant("minecraft:silk_touch", 1);
Some(pick)
}
other => Some(ItemStack::new(&format!("minecraft:{other}"), 1)),
}
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let [base, work, out, wanted @ ..] = args.as_slice() else {
return Err("usage: mod_port_probe PREPARED WORK OUT.json [MOD...]".into());
};
let (base, work) = (Path::new(base), Path::new(work));
let oracle: Value = serde_json::from_slice(&fs::read(work.join("oracle/facts.json"))?)?;
let mut mods = Vec::new();
for entry in fs::read_dir(work.join("mods"))?.flatten() {
let id = entry.file_name().to_string_lossy().into_owned();
let dir = entry.path().join("carry");
if !(wanted.is_empty() || wanted.contains(&id)) || !dir.is_dir() {
continue;
}
let package = horizon_mods::Package::read(&dir)?;
let content = horizon_mods::Content::read(&dir, &package.manifest)?;
mods.push(ContentMod {
id: package.manifest.id.clone(),
content,
});
}
mods.sort_by(|a, b| a.id.cmp(&b.id));
let root = work.join("horizon-root");
let began = std::time::Instant::now();
let step = |what: &str| {
let what = what.to_owned();
move |e: Box<dyn std::error::Error>| format!("{what}: {e}")
};
mod_content::build(base, &mods, &root).map_err(step("content root"))?;
let built = began.elapsed().as_secs_f64();
let assets = Assets::load(&root).map_err(step("assets"))?;
let registry = assets.registry;
let facts = BlockFacts::load(&root, registry).map_err(step("block facts"))?;
let items = Items::load(&root, registry, &facts).map_err(step("items"))?;
let recipes = Recipes::load(&root, &items).map_err(step("recipes"))?;
let stations = StationRecipes::load(&root, &items);
let collisions =
horizon_client::collisions::load(&root, registry).map_err(step("collisions"))?;
let loaded = began.elapsed().as_secs_f64() - built;
let ours = |id: &str| {
id.split_once(':').is_some_and(|(ns, _)| {
mods.iter()
.any(|m| m.content.namespaces.iter().any(|n| n == ns))
})
};
let tools: Vec<String> = serde_json::from_value(oracle["tools"].clone())?;
let mut blocks = Map::new();
for (name, block) in oracle["blocks"].as_object().ok_or("oracle: no blocks")? {
if !ours(name) {
continue;
}
let Some(default) = registry.default_state(name) else {
blocks.insert(name.clone(), Value::Null);
continue;
};
let mut states = Map::new();
for (key, o) in block["states"].as_object().into_iter().flatten() {
let properties: BTreeMap<String, String> = key
.split(',')
.filter(|p| !p.is_empty())
.filter_map(|p| p.split_once('='))
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect();
let descriptor = BlockState {
name: name.clone(),
properties,
};
let Some(id) = registry.id(&descriptor) else {
states.insert(key.clone(), Value::Null);
continue;
};
let f = facts.get(id);
let collision = collisions
.cached_shapes()
.cached_shape(id)
.ok()
.flatten()
.map(|s| {
boxes(s.boxes().iter().map(|b| {
[
b.min(Axis::X),
b.min(Axis::Y),
b.min(Axis::Z),
b.max(Axis::X),
b.max(Axis::Y),
b.max(Axis::Z),
]
}))
});
let mut row = json!({
"hardness": f.hardness,
"explosion_resistance": f.explosion_resistance,
"emission": f.emission,
"requires_tool": f.requires_tool,
"friction": f.friction,
"map_color_id": f.map_color,
"can_occlude": f.can_occlude,
"sound": sound(&f.sounds),
"outline": boxes(f.outline.iter().copied()),
"collision": collision,
"classes": f.classes.iter().map(|c| &**c).collect::<Vec<_>>(),
});
if o.get("drops").is_some() {
let mut drops = Map::new();
for t in &tools {
let mut random = LegacyRandom::new(0);
let held = tool(t);
let mut counts: BTreeMap<String, u32> = BTreeMap::new();
for s in items.block_drops(registry, id, held.as_ref(), &mut random) {
*counts.entry(s.item.to_string()).or_default() += s.count;
}
drops.insert(t.clone(), json!(counts));
}
row["drops"] = Value::Object(drops);
}
states.insert(key.clone(), row);
}
let item = facts.get(default).item.to_string();
blocks.insert(
name.clone(),
json!({"states": states, "item": item, "item_known": items.items.contains_key(&*item)}),
);
}
// Vanilla's sounds, by block: the harness maps the oracle's version's
// sound events to this one's through them.
let mut vanilla_sounds = Map::new();
for name in oracle["blocks"]
.as_object()
.into_iter()
.flatten()
.map(|(k, _)| k)
{
if let Some(id) = name
.starts_with("minecraft:")
.then(|| registry.default_state(name))
.flatten()
{
vanilla_sounds.insert(name.clone(), sound(&facts.get(id).sounds));
}
}
let mut item_rows = Map::new();
for name in oracle["items"]
.as_object()
.into_iter()
.flatten()
.map(|(k, _)| k)
{
item_rows.insert(
name.clone(),
match items.items.get(name.as_str()) {
Some(i) => json!({"max_stack_size": i.max_stack, "block": i.block.map(|b| b.0)}),
None => Value::Null,
},
);
}
let sorted = |list: &[String]| {
let mut v = list.to_vec();
v.sort();
v.dedup();
v
};
let mut recipe_rows = Map::new();
for (name, recipe) in &recipes.crafting {
if !ours(name) {
continue;
}
let row = match recipe {
Recipe::Shaped { cells, result, .. } => json!({
"result": result.item.as_deref(), "count": result.count,
"ingredients": cells.iter().flatten().map(|i| sorted(&i.1)).collect::<Vec<_>>()}),
Recipe::Shapeless {
ingredients,
result,
} => json!({
"result": result.item.as_deref(), "count": result.count,
"ingredients": ingredients.iter().map(|i| sorted(&i.1)).collect::<Vec<_>>()}),
Recipe::Special(_) => json!({"special": true}),
};
recipe_rows.insert(name.clone(), row);
}
for c in recipes.cooking.iter().filter(|c| ours(&c.id)) {
recipe_rows.insert(
c.id.clone(),
json!({"result": &*c.result.item, "count": c.result.count,
"ingredients": [sorted(&c.ingredient.1)]}),
);
}
for s in stations.stonecutting.iter().filter(|s| ours(&s.id)) {
recipe_rows.insert(
s.id.clone(),
json!({"result": &*s.result.item, "count": s.result.count,
"ingredients": [sorted(&s.input.1)]}),
);
}
let mut block_tags = Map::new();
for (tag, members) in &items.block_tags {
let mut ids: Vec<&String> = members.iter().filter(|m| ours(m)).collect();
if !ids.is_empty() {
ids.sort();
block_tags.insert(tag.clone(), json!(ids));
}
}
let report = json!({
"states": registry.len(),
"build_seconds": built,
"load_seconds": loaded,
"blocks": blocks,
"items": item_rows,
"recipes": recipe_rows,
"block_tags": block_tags,
"vanilla_sounds": vanilla_sounds,
});
fs::write(out, serde_json::to_vec(&report)?)?;
println!(
"{out}: {} blocks, {} items, {} recipes; root built in {built:.2} s, loaded in {loaded:.2} s",
blocks.len(),
item_rows.len(),
recipe_rows.len()
);
Ok(())
}
@@ -1,130 +0,0 @@
//! Full block/assets + item startup. Compile with HORIZON_BENCH_COUNT_ALLOCATIONS=1
//! for live requested heap bytes; leave it unset for latency measurements.
//! cargo run --profile fast -p horizon-client --example model_library_bench -- ROOT [--parity]
//! Run fresh processes, warm up twice, then compare multiple alternating baseline/candidate runs.
use horizon_client::{assets::Assets, item_render::ItemRenderer};
use std::{
alloc::{GlobalAlloc, Layout, System},
hash::{Hash, Hasher},
path::Path,
sync::atomic::{AtomicUsize, Ordering::Relaxed},
time::Instant,
};
struct CountingAllocator;
const TRACK: bool = option_env!("HORIZON_BENCH_COUNT_ALLOCATIONS").is_some();
static LIVE: AtomicUsize = AtomicUsize::new(0);
// The allocator delegates every operation and layout unchanged to System.
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if TRACK && !ptr.is_null() {
LIVE.fetch_add(layout.size(), Relaxed);
}
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc_zeroed(layout) };
if TRACK && !ptr.is_null() {
LIVE.fetch_add(layout.size(), Relaxed);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if TRACK {
LIVE.fetch_sub(layout.size(), Relaxed);
}
unsafe { System.dealloc(ptr, layout) };
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
let out = unsafe { System.realloc(ptr, layout, size) };
if TRACK && !out.is_null() {
LIVE.fetch_sub(layout.size(), Relaxed);
LIVE.fetch_add(size, Relaxed);
}
out
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn main() -> horizon_client::Result<()> {
let root = std::env::args()
.nth(1)
.ok_or("usage: model_library_bench ROOT [--parity]")?;
let root = Path::new(&root);
let initial = LIVE.load(Relaxed);
let start = Instant::now();
let assets = Assets::load(root)?;
let render = assets.reload(root)?;
let block_ms = start.elapsed().as_secs_f64() * 1000.;
let block_retained = LIVE.load(Relaxed) - initial;
let start_items = Instant::now();
let items = ItemRenderer::load_with_library(root, render.model_library.clone())?;
let item_ms = start_items.elapsed().as_secs_f64() * 1000.;
let total_ms = start.elapsed().as_secs_f64() * 1000.;
let retained = LIVE.load(Relaxed) - initial;
println!(
"block_ms={block_ms:.3} item_ms={item_ms:.3} total_ms={total_ms:.3} retained_bytes={retained} block_retained_bytes={block_retained}"
);
if std::env::args().any(|a| a == "--parity") {
let mut hash = std::collections::hash_map::DefaultHasher::new();
for id in 0..assets.registry.len() {
let id = horizon_world::registry::BlockStateId(id as u32);
for pos in [
horizon_core::position::BlockPos::new(0, 0, 0),
horizon_core::position::BlockPos::new(-17, 65, 31),
] {
if let Ok(selected) = render.model_catalog.collect(id, pos) {
for (variant, model) in selected {
format!("{variant:?}").hash(&mut hash);
model.ambient_occlusion.hash(&mut hash);
for face in &model.faces {
format!("{:?}", face.quad).hash(&mut hash);
face.layer.hash(&mut hash);
face.opaque.hash(&mut hash);
face.translucent.hash(&mut hash);
}
}
}
}
}
let mut names = Vec::new();
for ns in std::fs::read_dir(root.join("assets"))? {
let ns = ns?;
let base = ns.path().join("models");
let mut stack = vec![base.clone()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries {
let path = entry?.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "json") {
names.push(format!(
"{}:{}",
ns.file_name().to_string_lossy(),
path.strip_prefix(&base)?
.with_extension("")
.to_string_lossy()
.replace('\\', "/")
));
}
}
}
}
names.sort();
for name in &names {
format!("{:?}", items.bake(name)).hash(&mut hash);
}
println!(
"parity_models={} fingerprint={:016x}",
names.len(),
hash.finish()
);
}
std::hint::black_box((&assets, &items));
Ok(())
}
@@ -17,16 +17,13 @@
//! a `mobs::trial_spawner::TrialSpawner` with the game time advancing and
//! print its states, events, spawns and ejected items.
//!
//! Vault cases (`mode=vault`, tools/test_vault_parity.py) tick a
//! `vault::Vault` the same way, with players moving and using keys on it.
//!
//! usage: natural_spawn_fixture RESOURCES INPUT OUTPUT
use horizon_client::{
assets::Assets,
biome::{Biomes, SpawnEntry},
blocks::BlockFacts,
collisions::{self, LookupCollisions, no_collision_empty},
items::{ItemStack, Items},
items::Items,
mobs::{
MobTypes, Species,
data::MobData,
@@ -38,12 +35,8 @@ use horizon_client::{
},
spawner::{Spawner, SpawnerAdded, SpawnerEvent, SpawnerLevel, SpawnerSpawn},
species::{self, Baby},
trial_spawner::{
Omen, TRIAL_OMEN_TICKS_PER_LEVEL, TrialEvent, TrialPlayer, TrialSpawner,
TrialSpawnerLevel, TrialState,
},
trial_spawner::{TrialEvent, TrialPlayer, TrialSpawner, TrialSpawnerLevel, TrialState},
},
vault::{Vault, VaultEvent, VaultLevel, VaultPlayer, VaultState},
};
use horizon_core::{
geometry::Aabb,
@@ -122,14 +115,8 @@ struct World<'a> {
placed: RefCell<Vec<([i32; 3], BlockStateId)>>,
/// The game time (trial cases advance it).
time: Cell<i64>,
/// The mobs a trial spawner added: UUID, position, height and box.
trial_mobs: RefCell<Vec<([i32; 4], [f64; 3], f32, Aabb)>>,
/// How many mobs a trial spawner added (their UUIDs count up).
trial_count: Cell<i32>,
/// Each player's omen and its amplifier (`/trial_omen:A`, `/bad_omen:A`).
omens: RefCell<Vec<Option<(Omen, i32)>>>,
/// Loot tables (trial cases).
loot: Option<std::sync::Arc<horizon_world::loot::LootData>>,
/// The mobs a trial spawner added: UUID, position and box.
trial_mobs: RefCell<Vec<([i32; 4], [f64; 3], Aabb)>>,
}
impl World<'_> {
fn biome_at(&self, x: i32, y: i32, z: i32) -> &str {
@@ -268,16 +255,10 @@ impl NaturalLevel for World<'_> {
self.biome_info(x, y, z)
.map_or(0.1, |b| b.creature_probability)
}
fn structures_at(
&self,
x: i32,
y: i32,
z: i32,
wanted: &dyn Fn(&str) -> bool,
) -> Vec<StructureAt> {
fn structures_at(&self, x: i32, y: i32, z: i32) -> Vec<StructureAt> {
let (cx, cz) = (x >> 4, z >> 4);
let mut out: Vec<StructureAt> = Vec::new();
for start in self.starts.iter().filter(|s| wanted(&s.id)) {
for start in &self.starts {
let b = start.bounds.0;
// Referenced: the start's box reaches the chunk's columns.
if b[3] < cx * 16 || b[0] > cx * 16 + 15 || b[5] < cz * 16 || b[2] > cz * 16 + 15 {
@@ -397,16 +378,6 @@ impl TrialSpawnerLevel for World<'_> {
eye: [p[0], p[1] + f64::from(1.62f32), p[2]],
creative: mode == "creative",
spectator: mode == "spectator",
position: *p,
height: 1.8,
alive: true,
omen: self
.omens
.borrow()
.get(i)
.copied()
.flatten()
.map(|(o, _)| o),
})
.collect()
}
@@ -429,38 +400,9 @@ impl TrialSpawnerLevel for World<'_> {
}
fn entity_block(&self, uuid: [i32; 4]) -> Option<[i32; 3]> {
let mobs = self.trial_mobs.borrow();
let (_, p, ..) = mobs.iter().find(|(u, ..)| *u == uuid)?;
let (_, p, _) = mobs.iter().find(|(u, ..)| *u == uuid)?;
Some(p.map(|v| v.floor() as i32))
}
fn entity_box(&self, uuid: [i32; 4]) -> Option<([f64; 3], f32)> {
let mobs = self.trial_mobs.borrow();
let (_, p, h, _) = mobs.iter().find(|(u, ..)| *u == uuid)?;
Some((*p, *h))
}
fn no_block_collision(&self, block: [i32; 3]) -> bool {
let lookup = |x: i32, y: i32, z: i32| self.state(x, y, z);
let mut scene = LookupCollisions {
block: &lookup,
binding: self.s.collision,
};
let state = self.state(block[0], block[1], block[2]);
self.s
.collision
.collision_shape(&mut scene, state, block)
.map_or(true, |shape| shape.is_empty())
}
fn loot_items(&self, table: &str, seed: i64) -> Vec<horizon_client::items::ItemStack> {
let Some(loot) = &self.loot else {
return Vec::new();
};
loot.roll_split(table, &mut LegacyRandom::new(seed))
.iter()
.map(horizon_client::structure_blocks::loot_item_with_components)
.collect()
}
fn seed(&self) -> i64 {
self.seed
}
}
/// The box a mob is left with by `finalizeSpawn` under the oracle's fixed random.
@@ -530,7 +472,7 @@ fn main() -> horizon_client::Result<()> {
let data = MobData::load(root, &facts);
let items = Items::tags_only(root)?;
// Trial spawner rewards: the loot tables and the configs.
let loot = if input.contains("mode=trial") || input.contains("mode=vault") {
let loot = if input.contains("mode=trial") {
horizon_client::mobs::trial_spawner::load_registry(root);
horizon_client::structure_blocks::loot_data(root)
} else {
@@ -639,19 +581,6 @@ fn main() -> horizon_client::Result<()> {
bounds: Bx(b),
});
}
// A player's third field: an omen and its amplifier.
let omens = indexed("p")
.iter()
.map(|p| {
let (effect, amplifier) = p.split('/').nth(2)?.split_once(':')?;
let omen = if effect == "trial_omen" {
Omen::Trial
} else {
Omen::Bad
};
Some((omen, amplifier.parse().ok()?))
})
.collect();
let players = indexed("p")
.into_iter()
.map(|p| {
@@ -738,9 +667,6 @@ fn main() -> horizon_client::Result<()> {
placed: RefCell::new(Vec::new()),
time: Cell::new(0),
trial_mobs: RefCell::new(Vec::new()),
trial_count: Cell::new(0),
omens: RefCell::new(omens),
loot: loot.clone(),
entities: RefCell::new(
case_entities
.iter()
@@ -780,9 +706,6 @@ fn main() -> horizon_client::Result<()> {
} else if mode == "trial" {
let loot = loot.as_ref().ok_or("no loot tables")?;
trial_case(&world, registry, loot, &get, &mut random, &mut log)?;
} else if mode == "vault" {
let loot = loot.as_ref().ok_or("no loot tables")?;
vault_case(world.players.clone(), loot, &get, &mut random, &mut log)?;
} else if mode == "gen" {
let mut gen_random = LegacyRandom::new(get("gseed", "0").parse()?);
// The level random of the spawn predicates: no creature's predicate
@@ -1049,22 +972,8 @@ fn trial_case(
let scale = size as f32;
let bounds = spawn_box(x, y, z, t.width as f32 * scale, t.height as f32 * scale);
l.entities.borrow_mut().push((r.id.to_owned(), bounds));
let uuid = [1, 0, 0, l.trial_count.get()];
l.trial_count.set(l.trial_count.get() + 1);
// Zombie.readAdditionalSaveData: IsBaby halves the box
// (getAgeScale); finalizeSpawn's fixed random never makes one.
let baby = match r
.entity
.get(&horizon_world::nbt::JavaString::from("IsBaby"))
{
Some(Tag::Byte(v)) => *v != 0,
Some(Tag::Int(v)) => *v != 0,
_ => false,
};
let height = t.height as f32 * scale * if baby { 0.5 } else { 1.0 };
l.trial_mobs
.borrow_mut()
.push((uuid, r.position, height, bounds));
let uuid = [1, 0, 0, l.trial_mobs.borrow().len() as i32];
l.trial_mobs.borrow_mut().push((uuid, r.position, bounds));
spawned.push_back(format!(
"{tick} spawn {} {} {} {} {}",
r.id,
@@ -1119,7 +1028,7 @@ fn trial_case(
for e in events {
match e {
TrialEvent::BlockUpdated => log.push(format!("{tick} update")),
TrialEvent::LevelEvent(id, p, data) => {
TrialEvent::LevelEvent(id, _, data) => {
// A spawn's events follow its tryAddFreshEntityWithPassengers,
// a reward's its items.
match id {
@@ -1127,13 +1036,7 @@ fn trial_case(
3014 => log.append(&mut items),
_ => {}
}
match id {
3020 | 3012 => log.push(format!(
"{tick} levent {id} {data} at {},{},{}",
p[0], p[1], p[2]
)),
_ => log.push(format!("{tick} levent {id} {data}")),
}
log.push(format!("{tick} levent {id} {data}"));
}
TrialEvent::Sound(name) => log.push(format!(
"{tick} sound minecraft:{name} {} {} {} 1.0 1.0",
@@ -1145,44 +1048,6 @@ fn trial_case(
"{tick} gevent minecraft:entity_place {} {} {}",
p[0], p[1], p[2]
)),
TrialEvent::TrialOmen(uuid) => {
let i = (uuid[3] - 1) as usize;
let mut omens = world.omens.borrow_mut();
let amplifier = omens.get(i).copied().flatten().map_or(0, |(_, a)| a);
log.push(format!("{tick} effect remove minecraft:bad_omen"));
log.push(format!(
"{tick} effect add minecraft:trial_omen {} 0",
TRIAL_OMEN_TICKS_PER_LEVEL * (amplifier + 1)
));
if let Some(o) = omens.get_mut(i) {
*o = Some((Omen::Trial, 0));
}
}
TrialEvent::Discard(uuid) => {
let mut mobs = world.trial_mobs.borrow_mut();
if let Some(i) = mobs.iter().position(|(u, ..)| *u == uuid) {
let (.., b) = mobs.remove(i);
world.entities.borrow_mut().retain(|(_, e)| *e != b);
}
}
TrialEvent::SoundAt(name, b, pitch) => log.push(format!(
"{tick} sound minecraft:{name} {} {} {} 1.0 {pitch:?}",
bits(f64::from(b[0]) + 0.5),
bits(f64::from(b[1]) + 0.5),
bits(f64::from(b[2]) + 0.5)
)),
TrialEvent::ItemSpawner {
item,
position,
delay,
} => log.push(format!(
"{tick} ispawner {} {} {} {} {} {delay}",
item.item,
item.count,
bits(position[0]),
bits(position[1]),
bits(position[2])
)),
TrialEvent::OminousSet(set) => {
ominous = set;
world
@@ -1236,195 +1101,3 @@ fn trial_case(
log.push(format!("next {}", random.next_int_bound(1000).unwrap_or(0)));
Ok(())
}
/// What a vault reads: the game time and the case's players, moved by `move`.
struct VaultWorld {
time: i64,
players: Vec<([f64; 3], String)>,
}
impl VaultLevel for VaultWorld {
fn game_time(&self) -> i64 {
self.time
}
fn players(&self) -> Vec<VaultPlayer> {
self.players
.iter()
.enumerate()
.map(|(i, (p, mode))| VaultPlayer {
uuid: [0, 0, 0, i as i32 + 1],
block: p.map(|v| v.floor() as i32),
spectator: mode == "spectator",
})
.collect()
}
}
/// A UUID's low 64 bits, as the oracle prints players.
fn uuid_low(u: &[i32; 4]) -> String {
((i64::from(u[2]) << 32) | i64::from(u[3] as u32)).to_string()
}
/// The display item and the connected players as the oracle prints them.
fn vault_shared(v: &Vault) -> String {
let mut connected: Vec<String> = v.connected_players.iter().map(uuid_low).collect();
connected.sort();
format!(
"{} [{}]",
v.display_item
.as_ref()
.map_or_else(|| "-".to_owned(), |s| format!("{}*{}", s.item, s.count)),
connected.join(",")
)
}
/// `VaultBlockEntity.Server.tick` `ticks` times from game time `t0` at `spos`
/// on a vault block in state `state` (ominous with `ominous=1`) loaded from
/// `snbt` (base64 SNBT), with the case's key uses (`use`) and player moves
/// (`move`) before each tick, as the oracle logs it.
fn vault_case(
players: Vec<([f64; 3], String)>,
loot: &horizon_world::loot::LootData,
get: &dyn Fn(&str, &'static str) -> String,
random: &mut LegacyRandom,
log: &mut Vec<String>,
) -> horizon_client::Result<()> {
let v: Vec<i32> = get("spos", "0,0,0")
.split(',')
.map(|n| n.parse().unwrap_or(0))
.collect();
let pos = [v[0], v[1], v[2]];
let mut state = VaultState::from_name(&get("state", "inactive")).ok_or("bad state")?;
let ominous = get("ominous", "0") == "1";
let text = horizon_client::skins::base64_decode(&get("snbt", "")).ok_or("bad snbt")?;
let tag = horizon_world::nbt::parse_snbt(&String::from_utf8(text)?)?.0;
let mut vault = Vault::load(&tag);
let schedule = |key: &str| -> Vec<Vec<String>> {
get(key, "")
.split(';')
.filter(|s| !s.is_empty())
.map(|s| s.split('/').map(str::to_owned).collect())
.collect()
};
let (uses, moves) = (schedule("use"), schedule("move"));
let ticks: i64 = get("ticks", "1").parse()?;
let t0: i64 = get("t0", "0").parse()?;
let seed: i64 = get("wseed", "0").parse()?;
let mut sequences = horizon_client::random_sequences::RandomSequences::default();
let mut world = VaultWorld { time: t0, players };
let center = pos.map(|v| f64::from(v) + 0.5);
for tick in 0..ticks {
world.time = t0 + tick;
let at = |s: &[String]| s[0].parse() == Ok(tick);
for m in moves.iter().filter(|m| at(m)) {
let v: Vec<f64> = m[2].split(',').map(|n| n.parse().unwrap_or(0.0)).collect();
world.players[m[1].parse::<usize>()?].0 = [v[0], v[1], v[2]];
}
let mut events = Vec::new();
let mut roll = |table: &str, random: &mut LegacyRandom, sequence: bool| {
let drops = match loot.random_sequence(table).filter(|_| sequence) {
Some(s) => loot.roll_split(table, sequences.get(s, seed)),
None => loot.roll_split(table, random),
};
drops
.iter()
.map(horizon_client::structure_blocks::loot_item_with_components)
.collect()
};
for u in uses.iter().filter(|u| at(u)) {
let player: usize = u[1].parse()?;
let mut stack = ItemStack::new(&u[2], u[3].parse()?);
let used = vault.use_item_on(
&world,
random,
pos,
state,
ominous,
[0, 0, 0, player as i32 + 1],
Some(&stack),
&mut roll,
&mut events,
);
if world.players[player].1 != "creative" {
stack.count -= used.unwrap_or(0);
}
vault_events(&mut events, &mut state, ominous, tick, center, &vault, log);
log.push(format!(
"{tick} use {} {}",
if used.is_some() { "ok" } else { "pass" },
stack.count
));
}
vault.server_tick(&world, random, pos, state, ominous, &mut roll, &mut events);
vault_events(&mut events, &mut state, ominous, tick, center, &vault, log);
}
let saved = vault.save();
let config = saved
.iter()
.find(|(k, _)| *k == "config")
.and_then(|(_, t)| match t {
Tag::Compound(c) => {
let mut keys: Vec<String> = c.keys().map(|k| k.to_string_lossy()).collect();
keys.sort();
Some(keys.join(","))
}
_ => None,
})
.unwrap_or_default();
let eject: Vec<String> = vault
.items_to_eject
.iter()
.map(|s| format!("{}*{}", s.item, s.count))
.collect();
log.push(format!(
"end state={} rewarded={} resumes={} eject={} total={} shared={} config={config}",
state.name(),
vault
.rewarded_players
.iter()
.map(uuid_low)
.collect::<Vec<_>>()
.join(","),
vault.state_updating_resumes_at,
eject.join(","),
vault.total_ejections_needed,
vault_shared(&vault),
));
log.push(format!("next {}", random.next_int_bound(1000).unwrap_or(0)));
Ok(())
}
/// Logs a vault's events and applies its state changes.
fn vault_events(
events: &mut Vec<VaultEvent>,
state: &mut VaultState,
ominous: bool,
tick: i64,
center: [f64; 3],
vault: &Vault,
log: &mut Vec<String>,
) {
for e in events.drain(..) {
match e {
VaultEvent::State(s) => {
*state = s;
log.push(format!("{tick} state {} {ominous}", s.name()));
}
VaultEvent::BlockUpdated => log.push(format!("{tick} update {}", vault_shared(vault))),
VaultEvent::LevelEvent(id, data) => log.push(format!("{tick} levent {id} {data}")),
VaultEvent::Sound(name, volume, pitch) => log.push(format!(
"{tick} sound minecraft:{name} {} {} {} {volume:?} {pitch:?}",
bits(center[0]),
bits(center[1]),
bits(center[2])
)),
VaultEvent::Eject(stack, v) => log.push(format!(
"{tick} item {} {} {} {} {} {} {} {}",
stack.item,
stack.count,
bits(center[0]),
bits(center[1] + 0.7 - 0.125),
bits(center[2]),
bits(v[0]),
bits(v[1]),
bits(v[2])
)),
}
}
}
@@ -1,10 +1,7 @@
#![forbid(unsafe_code)]
//! Loads and saves option files like `Options.load`/`save`, for the
//! differential harness (tools/test_options_parity.py).
use horizon_client::{
Result, keybinds,
options_file::{OptionsFile, V},
};
use horizon_client::{Result, keybinds, options_file::OptionsFile};
use std::{fs, io::Write};
fn main() -> Result<()> {
@@ -17,16 +14,11 @@ fn main() -> Result<()> {
let mut first = true;
for path in names {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let mut file = if name == "absent.txt" {
let file = if name == "absent.txt" {
OptionsFile::default()
} else {
OptionsFile::parse(&String::from_utf8_lossy(&fs::read(&path)?))
};
// Horizon extends the slider to 64; vanilla rejects 33..64 to its default.
// Compare that deliberate extension as vanilla would serialize it.
if file.int("renderDistance") > 32 {
file.set("renderDistance", V::Int(12));
}
write!(out, "== {name}\n{}", file.serialize())?;
if first {
first = false;
@@ -112,26 +112,21 @@ fn state(s: &ParticleSnapshot) -> String {
}
out
}
fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Result<World> {
fn world(assets: &Assets, facts: &BlockFacts, floor: bool) -> Result<World> {
let mut world = World::new(facts.air, 0);
let state = |name: &str| assets.registry.default_state(name).ok_or("missing block");
let (stone, still) = (state("minecraft:stone")?, state("minecraft:water")?);
let stone = assets
.registry
.default_state("minecraft:stone")
.ok_or("missing stone")?;
for cz in -3..=2 {
for cx in -3..=2 {
world
.chunks
.insert((cx, cz), Arc::new(Chunk::empty(cx, cz)));
for z in 0..16 {
for x in 0..16 {
let (x, z) = (cx * 16 + x, cz * 16 + z);
if floor {
world.set(x, 63, z, stone)?;
}
// The oracle's water: still sources from y 64 to 67.
if water {
for y in 64..=67 {
world.set(x, y, z, still)?;
}
if floor {
for z in 0..16 {
for x in 0..16 {
world.set(cx * 16 + x, 63, cz * 16 + z, stone)?;
}
}
}
@@ -142,15 +137,13 @@ fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Resul
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = &Path::new(".local/horizon-client").join(horizon_core::VANILLA_VERSION);
let root = Path::new(".local/horizon-client/26.3");
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
let collision = collisions::load(root, assets.registry)?;
let worlds = [
world(&assets, &facts, false, false)?,
world(&assets, &facts, true, false)?,
world(&assets, &facts, false, true)?,
world(&assets, &facts, true, true)?,
world(&assets, &facts, false)?,
world(&assets, &facts, true)?,
];
let mut engine = ParticleEngine::new(root, &HashMap::new());
for (ty, item) in [
@@ -181,8 +174,7 @@ fn main() -> Result<()> {
engine.requested = Some(Vec::new());
engine.sounds.clear();
let level = ParticleLevel {
world: &worlds[usize::from(case["floor"].as_bool() == Some(true))
+ 2 * usize::from(case["water"].as_bool() == Some(true))],
world: &worlds[usize::from(case["floor"].as_bool() == Some(true))],
facts: &facts,
collision: &collision,
player: None,
@@ -1,96 +0,0 @@
//! Block entity data on items for tools/test_pick_block_data_parity.py: reads
//! one JSON case per line (see VanillaPickBlockDataOracle) and prints the
//! picked item's component patch, a tab, then the placed block entity.
//!
//! usage: pick_block_data_fixture INPUT OUTPUT
use horizon_client::block_entity_item::{pick, place};
use horizon_client::block_entity_nbt::canonical;
use horizon_client::java_text::{double_to_string, float_to_string};
use horizon_world::nbt::{Tag, parse_snbt};
use serde_json::Value;
use std::fs;
/// ParityData.canon: sorted keys, typed numbers, Java's float text.
fn canon(tag: &Tag, out: &mut String) {
let join = |out: &mut String, prefix: &str, items: Vec<String>| {
out.push_str(prefix);
out.push_str(&items.join(","));
out.push(']');
};
match tag {
Tag::Float(v) => out.push_str(&format!("{}f", float_to_string(f32::from_bits(*v)))),
Tag::Double(v) => out.push_str(&format!("{}d", double_to_string(f64::from_bits(*v)))),
Tag::List(items) => join(
out,
"[",
items
.iter()
.map(|t| {
let mut s = String::new();
canon(t, &mut s);
s
})
.collect(),
),
Tag::Compound(map) => {
let mut keys: Vec<_> = map.iter().map(|(k, v)| (k.to_string_lossy(), v)).collect();
keys.sort_by(|a, b| a.0.cmp(&b.0));
out.push('{');
for (i, (k, v)) in keys.into_iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&k);
out.push(':');
canon(v, out);
}
out.push('}');
}
other => out.push_str(&horizon_client::item_data::canon_tag(other)),
}
}
fn snbt(value: &Value) -> Option<Tag> {
parse_snbt(value.as_str()?).ok().map(|(tag, _)| tag)
}
fn case(line: &str) -> Option<String> {
let case: Value = serde_json::from_str(line).ok()?;
let saved = canonical(&snbt(&case["nbt"])?)?;
let picked = pick(&saved);
let patch = match case.get("patch") {
Some(p) => match snbt(p)? {
Tag::Compound(c) => c,
_ => return None,
},
None => picked.clone(),
};
let target = canonical(&snbt(&case["target"])?)?;
let Tag::Compound(mut placed) = place(&target, &patch, case["op"].as_bool()?) else {
return None;
};
// keepPacked is the chunk's, not saveWithFullMetadata's.
placed.remove(&"keepPacked".into());
let mut out = String::new();
canon(&Tag::Compound(picked), &mut out);
out.push('\t');
canon(&Tag::Compound(placed), &mut out);
Some(out)
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let input = fs::read_to_string(
args.get(1)
.ok_or("usage: pick_block_data_fixture INPUT OUTPUT")?,
)?;
let lines: Vec<String> = input
.lines()
.map(|line| case(line).unwrap_or_else(|| "error".to_owned()))
.collect();
fs::write(
args.get(2).ok_or("missing OUTPUT")?,
lines.join("\n") + "\n",
)?;
Ok(())
}
@@ -38,19 +38,12 @@ fn main() -> horizon_client::Result<()> {
let recipes = Recipes::load(root, &items)?;
let brewing = horizon_client::brewing::Brewing::load(root, &items);
let banners = horizon_client::banner::Banners::default();
let station_recipes = horizon_client::stations::StationRecipes::default();
let enchantments = horizon_client::enchant::Enchantments::default();
let names = horizon_client::creative::Names::default();
let env = Env {
items: &items,
recipes: &recipes,
brewing: &brewing,
banners: &banners,
station_recipes: &station_recipes,
enchantments: &enchantments,
names: &names,
trade: None,
maps: None,
};
let mut out = String::new();
for (line, order) in cases.lines().zip(orders.lines()) {
@@ -1,72 +0,0 @@
use horizon_client::player_sync::{PositionSender, teleport_velocity};
use horizon_core::geometry::Vec3;
use horizon_protocol::packets::play::{PlayerPosition, Serverbound as S};
use serde_json::Value;
use std::io::{BufRead, Write};
fn number(value: &Value) -> f64 {
f64::from_bits(u64::from_str_radix(value.as_str().unwrap(), 16).unwrap())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().collect();
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
for line in input.lines() {
let row: Value = serde_json::from_str(&line?)?;
if row["kind"] == "movement" {
let mut sender = PositionSender::default();
for op in row["ops"].as_array().unwrap() {
let pos = std::array::from_fn(|i| number(&op[i]));
let rot = (number(&op[3]) as f32, number(&op[4]) as f32);
if let Some(packet) =
sender.update(pos, rot, op[5].as_bool().unwrap(), op[6].as_bool().unwrap())
{
let name = match packet {
S::MovePlayerPos(_) => "pos",
S::MovePlayerPosRot(_) => "posrot",
S::MovePlayerRot(_) => "rot",
S::MovePlayerStatus(_) => "status",
_ => unreachable!(),
};
write!(out, "{name} ")?;
for byte in &packet.encode()[1..] {
write!(out, "{byte:02x}")?;
}
writeln!(out)?;
} else {
writeln!(out, "-")?;
}
}
} else {
let vector = |key: &str| {
Vec3::new(
number(&row[key][0]),
number(&row[key][1]),
number(&row[key][2]),
)
};
let rotation = |key: &str| (number(&row[key][0]) as f32, number(&row[key][1]) as f32);
let old = rotation("old");
let new = rotation("new");
let delta = vector("delta");
let packet = PlayerPosition {
teleport: 0,
position: [0.0; 3],
delta: [delta.x, delta.y, delta.z],
yaw: new.0,
pitch: new.1,
relatives: row["flags"].as_i64().unwrap() as i32,
};
let v = teleport_velocity(vector("velocity"), old, new, &packet);
writeln!(
out,
"{:016x} {:016x} {:016x}",
v.x.to_bits(),
v.y.to_bits(),
v.z.to_bits()
)?;
}
}
Ok(())
}
@@ -1,263 +0,0 @@
//! The `entity_portals` section scan only: `may_hold_portal` over synthetic
//! entity reaches, with the per-tick palette map against the resident cache.
//! Run with prepared client resources:
//! cargo run --profile fast -p horizon-client --example portal_scan_bench -- RESOURCES
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS, Section},
fast_hash::FxHashMap,
portal_sections::PortalSections,
};
use horizon_core::{
geometry::{Aabb, Vec3},
position::BlockPos,
};
use horizon_world::{level::Behaviours, registry::BlockStateId};
use std::{hint::black_box, path::Path, sync::Arc, time::Instant};
/// `Game::swept_portal`'s reach (`Entity.checkInsideBlocks`'s swept box).
fn reach(from: Vec3, to: Vec3, width: f32, height: f32) -> Aabb {
let half = f64::from(width) / 2.0;
let bounds = Aabb::new(
Vec3::new(to.x - half, to.y, to.z - half),
Vec3::new(to.x + half, to.y + f64::from(height), to.z + half),
)
.deflate(Vec3::new(1.0e-5, 1.0e-5, 1.0e-5));
bounds
.union_bounds(bounds.move_by(from.subtract(to)))
.inflate(Vec3::new(1.0, 1.0, 1.0))
}
fn sections(reach: Aabb) -> (i64, i32, i32, i32, i32, i32, i32) {
let (min, max) = (reach.min_position(), reach.max_position());
let (min, max) = (BlockPos::containing(min), BlockPos::containing(max));
let top = SECTIONS as i32 - 1;
let (x0, x1) = (min.x >> 4, max.x >> 4);
let (z0, z1) = (min.z >> 4, max.z >> 4);
let (y0, y1) = (
((min.y - MIN_Y) >> 4).clamp(0, top),
((max.y - MIN_Y) >> 4).clamp(0, top),
);
let count = (i64::from(x1) - i64::from(x0) + 1)
* (i64::from(y1) - i64::from(y0) + 1)
* (i64::from(z1) - i64::from(z0) + 1);
(count, x0, x1, y0, y1, z0, z1)
}
/// The original `Game::may_hold_portal`: per-tick `seen`, palette per section.
fn may_hold_portal_baseline(
reach: Aabb,
world: &FxHashMap<(i32, i32), Arc<Chunk>>,
seen: &mut FxHashMap<(i32, i32, i32), bool>,
behaviours: &Behaviours,
) -> bool {
let (count, x0, x1, y0, y1, z0, z1) = sections(reach);
if count > 64 {
return true;
}
(x0..=x1).any(|cx| {
(z0..=z1).any(|cz| {
(y0..=y1).any(|sy| {
*seen.entry((cx, sy, cz)).or_insert_with(|| {
world
.get(&(cx, cz))
.and_then(|c| c.sections.get(sy as usize))
.is_some_and(|s| {
s.blocks
.maybe_has(|v| behaviours.portal_axis(BlockStateId(v)).is_some())
})
})
})
})
})
}
/// The cached `Game::may_hold_portal`: one mask per resident chunk.
fn may_hold_portal_cached(
reach: Aabb,
world: &FxHashMap<(i32, i32), Arc<Chunk>>,
cache: &mut PortalSections,
behaviours: &Behaviours,
) -> bool {
let (count, x0, x1, y0, y1, z0, z1) = sections(reach);
if count > 64 {
return true;
}
let mut y_mask = 0u32;
for sy in y0..=y1 {
y_mask |= 1 << sy;
}
(x0..=x1).any(|cx| {
(z0..=z1).any(|cz| {
world.get(&(cx, cz)).is_some_and(|chunk| {
cache.get((cx, cz), chunk, |v| {
behaviours.portal_axis(BlockStateId(v)).is_some()
}) & y_mask
!= 0
})
})
})
}
fn main() -> horizon_client::Result<()> {
let root = std::env::args()
.nth(1)
.ok_or("expected prepared client resource directory")?;
let start = Instant::now();
let registry = horizon_world::level::data::load_registry(Path::new(&root))?;
let (facts, behaviours) = BlockFacts::load_pair(Path::new(&root), &registry)?;
println!(
"resource_load_ms={:.3}",
start.elapsed().as_secs_f64() * 1000.0
);
let len = registry.len() as u32;
let portal: Vec<u32> = (0..len)
.filter(|&id| behaviours.portal_axis(BlockStateId(id)).is_some())
.collect();
let inactive_states: Vec<u32> = (0..len)
.filter(|&id| behaviours.portal_axis(BlockStateId(id)).is_none())
.collect();
let _ = &facts;
let portal_state = *portal
.first()
.ok_or("no nether portal block state in the registry")?;
println!("portal_states={}", portal.len());
// 500 entities spread over a 17x17 chunk world. Their reaches touch 1-4
// sections, as an item or a mob standing in or stepping into a portal does.
let grid = 17;
let base = -(grid / 2) * 16;
let entities: Vec<(Vec3, Vec3, f32, f32)> = (0..500)
.map(|i| {
let x = base + (i % grid) * 16 + (i % 5);
let z = base + (i / grid % grid) * 16 + (i % 7);
let y = 64 + (i % 3) * 16;
let from = Vec3::new(f64::from(x), f64::from(y), f64::from(z));
let to = from.add_vector(Vec3::new(0.1, 0.0, 0.07));
(from, to, 0.25, 0.25)
})
.collect();
let reaches: Vec<Aabb> = entities
.iter()
.map(|&(from, to, w, h)| reach(from, to, w, h))
.collect();
for (name, palette, portals_every) in [
("sparse", 1usize, 0usize),
("dense", 128, 0),
("global", 300, 0),
("dense_portal", 128, 5),
] {
let start = Instant::now();
let mut world: FxHashMap<(i32, i32), Arc<Chunk>> = FxHashMap::default();
for czi in 0..grid {
for cxi in 0..grid {
let (cx, cz) = (-(grid / 2) + cxi, -(grid / 2) + czi);
let mut chunk = Chunk::empty(cx, cz);
for (i, section) in chunk.sections.iter_mut().enumerate() {
let values: [u32; 4096] = std::array::from_fn(|j| {
if portals_every > 0 && i % portals_every == 0 && j == 0 {
portal_state
} else {
inactive_states[(j * 7 + i) % palette.min(inactive_states.len())]
}
});
Arc::make_mut(section).blocks = Section::from_blocks(&values);
}
world.insert((cx, cz), Arc::new(chunk));
}
}
println!(
"{name} fixture_ms={:.3}",
start.elapsed().as_secs_f64() * 1000.0
);
let keys: Vec<_> = world.keys().copied().collect();
let portal_chunks: Vec<(i32, i32)> = keys
.iter()
.copied()
.filter(|&(cx, cz)| {
world[&(cx, cz)]
.sections
.iter()
.any(|s| s.blocks.maybe_has(|v| v == portal_state))
})
.collect();
println!("{name} portal_chunks={}", portal_chunks.len());
// The global palette's maybe_has walks the whole ID range, so rescanning
// every chunk each tick (cold/reload/edit_all) is intractable; those rows
// are only meaningful for the palettes whose scans are bounded.
let workloads: &[(&str, usize)] = if name == "global" {
&[("steady", 0), ("edit_1pct", 3)]
} else {
&[
("steady", 0),
("cold", 0),
("reload", 0),
("edit_1pct", 3),
("edit_all", 289),
]
};
for &(workload, changed) in workloads {
let mut baseline = Vec::new();
let mut candidate = Vec::new();
let mut cache = PortalSections::default();
let (mut base_hits, mut cand_hits) = (0u64, 0u64);
for sample in 0..8 {
for cached in if sample % 2 == 0 {
[false, true]
} else {
[true, false]
} {
let start = Instant::now();
let mut result = 0u64;
for tick in 0..300 {
if cached && workload == "cold" {
cache = PortalSections::default();
}
if cached && workload == "reload" {
cache.clear();
}
for key in keys.iter().take(changed) {
Arc::make_mut(world.get_mut(key).unwrap()).set(
0,
MIN_Y,
0,
inactive_states[tick % inactive_states.len()],
);
}
let mut seen = FxHashMap::default();
for reach in &reaches {
let hit = if cached {
may_hold_portal_cached(*reach, &world, &mut cache, &behaviours)
} else {
may_hold_portal_baseline(*reach, &world, &mut seen, &behaviours)
};
result += hit as u64;
}
}
if cached {
cache = PortalSections::default();
cand_hits += black_box(result);
} else {
base_hits += black_box(result);
}
if sample > 0 {
let us = start.elapsed().as_secs_f64() * 1e6 / 300.0;
if cached {
candidate.push(us);
} else {
baseline.push(us);
}
}
}
}
assert_eq!(
base_hits, cand_hits,
"{name} {workload}: cached may_hold_portal diverged"
);
println!("{name} {workload} baseline_us={baseline:.3?} candidate_us={candidate:.3?}");
}
}
println!("chunk_wrapper_bytes={}", std::mem::size_of::<Chunk>());
Ok(())
}
@@ -1,149 +0,0 @@
//! Selection cost only (not block simulation). Run with prepared client resources.
//! cargo run --profile fast -p horizon-client --example random_tick_eligibility_bench -- RESOURCES
use horizon_client::{
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, Section},
fast_hash::FxHashMap,
random_tick_sections::RandomTickSections,
};
use horizon_world::registry::BlockStateId;
use std::{hint::black_box, path::Path, sync::Arc, time::Instant};
fn select_baseline(chunks: &FxHashMap<(i32, i32), Arc<Chunk>>, facts: &BlockFacts) -> usize {
let mut ticking = Vec::new();
for (&(x, z), chunk) in chunks {
if x.abs() > 8 || z.abs() > 8 || !chunk.lit {
continue;
}
let sections: Vec<i32> = chunk
.sections
.iter()
.enumerate()
.filter(|(_, s)| {
s.blocks
.maybe_has(|id| facts.get(BlockStateId(id)).random_ticks)
})
.map(|(i, _)| MIN_Y + i as i32 * 16)
.collect();
ticking.push((x, z, sections));
}
black_box(ticking).iter().map(|(_, _, s)| s.len()).sum()
}
fn select_cached(
chunks: &FxHashMap<(i32, i32), Arc<Chunk>>,
facts: &BlockFacts,
cache: &mut RandomTickSections,
) -> usize {
let mut ticking = Vec::new();
for (&(x, z), chunk) in chunks {
if x.abs() > 8 || z.abs() > 8 || !chunk.lit {
continue;
}
ticking.push((x, z, cache.get((x, z), chunk, facts)));
}
black_box(ticking).iter().map(|(_, _, s)| s.len()).sum()
}
fn main() -> horizon_client::Result<()> {
let root = std::env::args()
.nth(1)
.ok_or("expected prepared client resource directory")?;
let start = Instant::now();
let registry = horizon_world::level::data::load_registry(Path::new(&root))?;
let facts = BlockFacts::load(Path::new(&root), &registry)?;
println!(
"resource_load_ms={:.3}",
start.elapsed().as_secs_f64() * 1000.0
);
let inactive: Vec<u32> = facts
.states
.iter()
.enumerate()
.filter(|(_, f)| !f.random_ticks)
.take(128)
.map(|(i, _)| i as u32)
.collect();
let active = registry.default_state("minecraft:grass_block").unwrap().0;
assert!(facts.get(BlockStateId(active)).random_ticks);
for (name, palette) in [("sparse", 1), ("dense", 128)] {
let start = Instant::now();
let mut chunks = FxHashMap::default();
for z in -8..=8 {
for x in -8..=8 {
let mut chunk = Chunk::empty(x, z);
chunk.lit = true;
for (i, section) in chunk.sections.iter_mut().enumerate() {
let mut values = std::array::from_fn(|j| inactive[j % palette]);
if i % 3 == 0 {
values[4095] = active;
}
Arc::make_mut(section).blocks = Section::from_blocks(&values);
}
chunks.insert((x, z), Arc::new(chunk));
}
}
println!(
"{name} fixture_ms={:.3}",
start.elapsed().as_secs_f64() * 1000.0
);
for (workload, changed) in [
("steady", 0),
("cold", 0),
("reload", 0),
("edit_1pct", 3),
("edit_all", 289),
] {
let keys: Vec<_> = chunks.keys().copied().collect();
let mut baseline = Vec::new();
let mut candidate = Vec::new();
let mut cache = RandomTickSections::default();
for sample in 0..8 {
// Alternate order; first pair warms both paths and is discarded.
for cached in if sample % 2 == 0 {
[false, true]
} else {
[true, false]
} {
let start = Instant::now();
for tick in 0..300 {
if cached && workload == "cold" {
cache = RandomTickSections::default();
}
if cached && workload == "reload" {
cache.clear();
}
for key in keys.iter().take(changed) {
Arc::make_mut(chunks.get_mut(key).unwrap()).set(
0,
MIN_Y,
0,
inactive[tick % 2],
);
}
black_box(if cached {
select_cached(&chunks, &facts, &mut cache)
} else {
select_baseline(&chunks, &facts)
});
}
// Include teardown, and remove weak handles before baseline edits.
if cached {
cache = RandomTickSections::default();
}
if sample > 0 {
let us = start.elapsed().as_secs_f64() * 1e6 / 300.0;
if cached {
candidate.push(us);
} else {
baseline.push(us);
}
}
}
}
println!("{name} {workload} baseline_us={baseline:.3?} candidate_us={candidate:.3?}");
}
}
println!("chunk_wrapper_bytes={}", std::mem::size_of::<Chunk>());
Ok(())
}
@@ -1,153 +0,0 @@
//! Resident memory of each resource loader, in the game's start order: what a
//! larger registry (content mods, docs/modpacks.md) costs, loader by loader.
//!
//! resource_memory RESOURCES
//!
//! RESOURCES is a prepared root or a content-mod root (`.horizon-cache/mod-content`).
//! Prints, after each step, the live heap (a counting allocator: what the
//! step keeps), the heap's peak during the step (what it parsed on the way)
//! and the process RSS (what the allocator holds on to). On macOS it ends
//! with `footprint`'s physical footprint, the number the OS charges: RSS also
//! counts freed pages the allocator marked reusable.
use horizon_client::{
assets::Assets, biome::Biomes, blocks::BlockFacts, collisions, crafting::Recipes, items::Items,
light::LightTables, mesher::MeshTables, models, motion::MotionResources,
vanillagen::VanillaTerrain,
};
use std::{
alloc::{GlobalAlloc, Layout, System},
sync::atomic::{AtomicUsize, Ordering::Relaxed},
};
use std::{path::PathBuf, process::Command};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
struct Counting;
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
// SAFETY: forwards to the system allocator, only counting sizes.
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc(layout) };
if !p.is_null() {
let now = LIVE.fetch_add(layout.size(), Relaxed) + layout.size();
PEAK.fetch_max(now, Relaxed);
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
LIVE.fetch_sub(layout.size(), Relaxed);
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new: usize) -> *mut u8 {
let p = unsafe { System.realloc(ptr, layout, new) };
if !p.is_null() {
if new >= layout.size() {
let now = LIVE.fetch_add(new - layout.size(), Relaxed) + new - layout.size();
PEAK.fetch_max(now, Relaxed);
} else {
LIVE.fetch_sub(layout.size() - new, Relaxed);
}
}
p
}
}
#[global_allocator]
static ALLOC: Counting = Counting;
const MB: f64 = 1048576.0;
fn rss_mb() -> f64 {
let out = Command::new("ps")
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
.output()
.ok();
out.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<f64>().ok())
.map_or(0.0, |kb| kb / 1024.0)
}
fn main() -> Result<()> {
let root = PathBuf::from(
std::env::args()
.nth(1)
.ok_or("usage: resource_memory RESOURCES")?,
);
println!(
"{:<24} {:>8} {:>8} {:>10} {:>8}",
"step", "live MB", "+live", "step peak", "RSS MB"
);
let mut last = 0usize;
let mut step = |name: &str| {
let live = LIVE.load(Relaxed);
let peak = PEAK.swap(live, Relaxed);
println!(
"{name:<24} {:>8.0} {:>+8.0} {:>10.0} {:>8.0}",
live as f64 / MB,
(live as f64 - last as f64) / MB,
peak as f64 / MB,
rss_mb()
);
last = live;
};
step("start");
let registry = horizon_world::level::data::load_registry(&root)?;
step("registry alone");
drop(registry);
step("registry dropped");
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
step("assets");
println!(
" sprites: {} layers of {}px, {:.0} MB of pixels",
render.texture_layers,
render.sprite_size,
render.pixels.len() as f64 / MB
);
// The game decodes gameplay facts once for both tables (BlockFacts::load_pair).
let (facts, behaviours) = BlockFacts::load_pair(&root, assets.registry)?;
step("block facts+behaviours");
let items = Items::load(&root, assets.registry, &facts)?;
step("items");
let recipes = Recipes::load(&root, &items)?;
step("recipes");
let biomes = Biomes::load(&root)?;
step("biomes");
let light = LightTables::load(&root, assets.registry, &facts)?;
step("light tables");
let tables = MeshTables::new(
&assets,
&render,
&facts,
models::load_visibility(&root, assets.registry)?,
)?;
step("mesh tables");
let collision = collisions::load(&root, assets.registry)?;
step("collision");
let motion = MotionResources::load_with_facts(&root, &assets, &facts)?;
step("motion");
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, 1234)?;
step("worldgen");
println!(
"{} block states; live {:.0} MB, RSS {:.0} MB",
facts.states.len(),
LIVE.load(Relaxed) as f64 / MB,
rss_mb()
);
if let Ok(out) = Command::new("footprint")
.arg(std::process::id().to_string())
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
for line in text
.lines()
.filter(|l| l.contains("Footprint") || l.contains("phys_footprint"))
{
println!(" {}", line.trim());
}
}
// Keep everything alive until the end.
drop((
terrain, behaviours, motion, collision, tables, light, biomes, recipes, items, facts,
assets,
));
Ok(())
}
@@ -4,7 +4,7 @@
//! Exact NBT equality is always validated. No GPU, compression or disk I/O timed.
use horizon_client::{
biome::Biomes,
chunk::{Chunk, block_strategy},
chunk::{BLOCK_STRATEGY, Chunk},
containers::BlockEntity,
items::ItemStack,
save::{self, ChunkExtras},
@@ -64,8 +64,9 @@ fn fixture(per_chunk: usize) -> (Vec<Arc<Chunk>>, Entities, Names) {
};
let mut template = Chunk::empty(0, 0);
for s in &mut template.sections[..8] {
Arc::make_mut(s).blocks = PalettedContainer::new(block_strategy(), 1);
Arc::make_mut(s).blocks = PalettedContainer::new(BLOCK_STRATEGY, 1);
}
template.heights = Arc::new([64; 256]);
for i in 0..256 {
let x = i % 16 - 8;
let z = i / 16 - 8;
@@ -1,128 +0,0 @@
//! The region writer on real chunks, without a window: how long a world's
//! chunks take to be encoded, compressed and written by `SaveDir`, against
//! the same encoding and compression done one chunk after another (what the
//! writer thread did alone before it had helpers).
//!
//! save_bench RESOURCES DIMENSION_DIR [--rounds N]
//!
//! DIMENSION_DIR is a saved dimension folder (`saves/W/dimensions/minecraft/
//! overworld`); its chunks are read once, then each round saves them all into
//! a scratch folder with `save_later` + `flush`, and times the one-thread
//! baseline. The written chunks are checked byte for byte against the
//! baseline's compressed records.
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
save::{SaveDir, SaveTables, encode_with},
};
use horizon_core::position::ChunkPos;
use horizon_world::{
compression::Compression,
nbt::Limits,
region::{ChunkRecord, RegionFile},
};
use std::{path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let usage = "usage: save_bench RESOURCES DIMENSION_DIR [--rounds N]";
let root = PathBuf::from(args.first().ok_or(usage)?);
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
let rounds: usize = args
.iter()
.position(|a| a == "--rounds")
.and_then(|i| args.get(i + 1))
.and_then(|v| v.parse().ok())
.unwrap_or(5);
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Arc::new(Biomes::load(&root)?);
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
let registry = assets.registry;
let source = SaveDir::new(dimension.clone());
let _ = source.tables.set(tables.clone());
let mut chunks = Vec::new();
for entry in std::fs::read_dir(dimension.join("region"))? {
let path = entry?.path();
if path.extension().is_none_or(|e| e != "mca") {
continue;
}
let region = RegionFile::open(&path)?;
for (pos, _) in region.chunks() {
if let Some(chunk) = source.load(pos.x, pos.z, registry, &biomes) {
chunks.push(Arc::new(chunk));
}
}
}
chunks.sort_by_key(|c| (c.x, c.z));
println!("{} chunks from {}", chunks.len(), dimension.display());
let scratch = std::env::temp_dir().join(format!("save_bench_{}", std::process::id()));
// Encoding and compressing on one thread (what the writer did alone),
// then SaveDir's writer with its helpers.
let mut times = [Vec::new(), Vec::new(), Vec::new()];
let mut written = Vec::new();
for round in 0..rounds {
let started = Instant::now();
let tags: Vec<_> = chunks
.iter()
.map(|c| encode_with(c, registry, &biomes, Some(&tables), true))
.collect();
times[0].push(started.elapsed().as_secs_f64());
let started = Instant::now();
let baseline: Vec<ChunkRecord> = tags
.iter()
.map(|tag| ChunkRecord::from_nbt(tag, Compression::Zlib, 0, Limits::default()))
.collect::<std::io::Result<_>>()?;
times[1].push(started.elapsed().as_secs_f64());
drop(tags);
let _ = std::fs::remove_dir_all(&scratch);
let save = SaveDir::new(scratch.clone());
let _ = save.tables.set(tables.clone());
let started = Instant::now();
save.save_later(&chunks, registry, &biomes)?;
save.flush().unwrap();
times[2].push(started.elapsed().as_secs_f64());
drop(save);
if round == 0 {
for (chunk, want) in chunks.iter().zip(&baseline) {
let path = scratch.join(format!("region/r.{}.{}.mca", chunk.x >> 5, chunk.z >> 5));
let got = RegionFile::open(&path)?
.read(ChunkPos::new(chunk.x, chunk.z))?
.ok_or("a saved chunk is missing")?;
if got.compressed != want.compressed {
return Err(format!("chunk {} {} differs", chunk.x, chunk.z).into());
}
}
written = baseline.iter().map(|r| r.compressed.len()).collect();
}
}
let _ = std::fs::remove_dir_all(&scratch);
println!(
"{} KiB compressed, identical to the one-thread records",
written.iter().sum::<usize>() / 1024
);
let per_chunk = 1000.0 / chunks.len() as f64;
let names = [
"one thread: encode",
"one thread: compress",
"SaveDir (encode, compress, write)",
];
for (name, t) in names.iter().zip(&mut times) {
t.sort_by(f64::total_cmp);
println!(
"{name}: best {:.0} ms, median {:.0} ms of {rounds} ({:.3} ms per chunk at best)",
t[0] * 1000.0,
t[t.len() / 2] * 1000.0,
t[0] * per_chunk
);
}
Ok(())
}
@@ -257,9 +257,9 @@ fn main() -> horizon_client::Result<()> {
);
Ok(())
}
fn tables_air(assets: &Assets) -> u32 {
fn tables_air(assets: &Assets) -> u16 {
assets
.registry
.default_state("minecraft:air")
.map_or(0, |s| s.0)
.map_or(0, |s| s.0 as u16)
}
@@ -2,11 +2,6 @@
#![forbid(unsafe_code)]
use horizon_client::{assets, collisions, world};
use horizon_core::geometry::{Aabb, Vec3};
use horizon_world::{
collision_context::CollisionContext,
entity_obstruction::EntityIdentity,
world_movement::{self, CollisionCheckRequest, MovementRequest},
};
use serde_json::Value;
use std::{
fs,
@@ -40,8 +35,7 @@ fn main() -> Result<()> {
return Err("radius must be 1..16".into());
}
let assets = assets::Assets::load(&resources)?;
let render = assets.reload(&resources)?;
let world = world::World::test_scene(&assets, &render, radius)?;
let world = world::World::test_scene(&assets, radius)?;
let binding = collisions::load(&resources, assets.registry)?;
let mut scene = collisions::SceneCollisions {
world: &world,
@@ -51,7 +45,7 @@ fn main() -> Result<()> {
fs::write(
dir.join("scene.json"),
serde_json::to_vec(&serde_json::json!({
"radius":radius,"resident_chunks":world.chunks.len(),"model_gallery":render.model_gallery,
"radius":radius,"resident_chunks":world.chunks.len(),"model_gallery":assets.model_gallery,
}))?,
)?;
let mut output = std::io::BufWriter::new(fs::File::create(dir.join("rust.txt"))?);
@@ -63,55 +57,21 @@ fn main() -> Result<()> {
f["step"].as_str().ok_or("expected step bits")?,
16,
)?);
let entities = f["entities"]
.as_array()
.into_iter()
.flatten()
.map(|v| {
let a = v.as_array().ok_or("expected entity box")?;
Ok(Aabb::new(
vector(&Value::Array(a[..3].to_vec()))?,
vector(&Value::Array(a[3..].to_vec()))?,
))
})
.collect::<Result<Vec<_>>>()?;
let mut selected = collisions::EntityCollisions {
blocks: scene,
entities: &entities,
};
let movement = world_movement::resolve_world_movement(
&mut selected,
MovementRequest {
entity: EntityIdentity(0),
context: CollisionContext::position(bounds.min_position().y),
bounds,
movement: delta,
max_step: step,
on_ground: f["grounded"].as_bool().ok_or("expected grounded flag")?,
candidate_budget: 1_000_000,
},
let movement = scene.resolve(
bounds,
delta,
step,
f["grounded"].as_bool().ok_or("expected grounded flag")?,
)?;
let mut unobstructed = |bounds: Aabb| {
world_movement::no_world_collision(
&mut selected,
CollisionCheckRequest {
entity: EntityIdentity(0),
context: CollisionContext::position(bounds.min_position().y),
bounds,
candidate_budget: 1_000_000,
},
)
};
writeln!(
output,
"move {:016x} {:016x} {:016x} clear {} {}",
movement.x.to_bits(),
movement.y.to_bits(),
movement.z.to_bits(),
unobstructed(bounds)?,
unobstructed(bounds.move_by(movement))?
scene.unobstructed(bounds)?,
scene.unobstructed(bounds.move_by(movement))?
)?;
scene = selected.blocks;
}
Ok(())
}
@@ -243,14 +243,13 @@ fn main() -> Result<()> {
let root = PathBuf::from(&args[0]);
let dir = PathBuf::from(&args[1]);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let mut scene = World::test_scene(&assets, &render, 1)?;
let mut scene = World::test_scene(&assets, 1)?;
let binding = collisions::load(&root, assets.registry)?;
let resources = MotionResources::load(&root, &assets)?;
fs::write(
dir.join("scene.json"),
serde_json::to_vec(
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":render.model_gallery}),
&serde_json::json!({"radius":1,"resident_chunks":scene.chunks.len(),"model_gallery":assets.model_gallery}),
)?,
)?;
if args[2] == "--describe" {
@@ -299,7 +298,6 @@ fn main() -> Result<()> {
let mut travel: Option<horizon_core::position::BlockPos> = None;
for op in f["ops"].as_array().unwrap() {
let mut world = MotionWorld {
entities: &[],
player: &mut player,
scene: &mut scene,
assets: &assets,
@@ -3,7 +3,7 @@
//! the oracle's canonical form (vanilla ARGB colours and packed light coords).
#![forbid(unsafe_code)]
use horizon_client::{
assets::{Assets, RenderAssets},
assets::Assets,
biome::{BiomeInfo, Biomes, GrassModifier},
blocks::BlockFacts,
chunk::{Chunk, Light, MIN_Y, index},
@@ -48,7 +48,6 @@ fn chunk_index(x: i32, z: i32) -> Result<usize> {
fn scene(
row: &Value,
assets: &Assets,
render: &RenderAssets,
tables: &MeshTables,
biomes: &mut Biomes,
pristine: &[BiomeInfo],
@@ -84,7 +83,7 @@ fn scene(
for block in row["blocks"].as_array().ok_or("missing blocks")? {
let (x, y, z) = (int(&block[0])?, int(&block[1])?, int(&block[2])?);
let i = chunk_index(x, z)?;
chunks[i].set(x & 15, y, z & 15, u32::try_from(int(&block[3])?)?);
chunks[i].set(x & 15, y, z & 15, u16::try_from(int(&block[3])?)?);
}
for cell in row["lights"].as_array().ok_or("missing lights")? {
let (x, y, z) = (int(&cell[0])?, int(&cell[1])?, int(&cell[2])?);
@@ -128,7 +127,6 @@ fn scene(
let mut mesher = Mesher {
nether_shading: row["nether"].as_bool().unwrap_or(false),
assets,
render,
tables,
biomes: &*biomes,
visibility: tables.visibility.clone(),
@@ -136,7 +134,6 @@ fn scene(
blend_radius,
smooth_lighting,
cutout_leaves,
cull_leaves: false,
};
let section_y = int(&row["section_y"])?;
let started = Instant::now();
@@ -161,8 +158,15 @@ fn scene(
let vertices: Vec<Value> = quad
.iter()
.map(|v| {
let ([x, y, z], [u, w]) = (v.pos, v.uv());
json!([x, y, z, u, w, argb(v.color), light_coords(v.light)])
json!([
v.pos[0],
v.pos[1],
v.pos[2],
v.uv[0],
v.uv[1],
argb(v.color),
light_coords(v.light)
])
})
.collect();
quads.push(json!({"layer": layer, "sprite": sprite, "vertices": vertices}));
@@ -179,12 +183,11 @@ fn main() -> Result<()> {
}
let root = PathBuf::from(&args[0]);
let assets = Assets::load(&root)?;
let render = assets.reload(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let mut biomes = Biomes::load(&root)?;
let visibility = models::load_visibility(&root, assets.registry)?;
let tables = MeshTables::new(&assets, &render, &facts, visibility)?;
let names: BTreeMap<u32, &str> = render
let tables = MeshTables::new(&assets, &facts, visibility)?;
let names: BTreeMap<u32, &str> = assets
.sprite_layers
.iter()
.map(|(name, layer)| (*layer, name.as_str()))
@@ -218,7 +221,6 @@ fn main() -> Result<()> {
let result = scene(
&row,
&assets,
&render,
&tables,
&mut biomes,
&pristine,
@@ -1,74 +0,0 @@
//! Profile the fact and behaviour table phase of client startup.
//! Run each mode in a separate process to capture CPU usage and peak RSS.
//! The deferred mode evaluates both phases contiguously for fair loader comparison;
//! actual client startup performs other initialization between these phases.
use horizon_client::{assets::Assets, blocks::BlockFacts};
use horizon_world::level::Behaviours;
use std::{path::Path, time::Instant};
fn resident_kib() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let line = status.lines().find(|line| line.starts_with("VmRSS:"))?;
line.split_whitespace().nth(1)?.parse().ok()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let root = args.next().ok_or("expected resource directory")?;
let mode = args
.next()
.ok_or("expected baseline, pr607, shared, deferred or deferred-held")?;
if !["baseline", "pr607", "shared", "deferred", "deferred-held"].contains(&mode.as_str()) {
return Err("expected baseline, pr607, shared, deferred or deferred-held".into());
}
let root = Path::new(&root);
let total = Instant::now();
let assets = Assets::load(root)?;
let started = Instant::now();
let mut retained_inputs = None;
let mut subphases = None;
let (facts, behaviours) = if mode == "baseline" {
(
BlockFacts::load(root, assets.registry)?,
Behaviours::load(assets.registry, root)?,
)
} else if mode == "pr607" {
// Original PR #607 sequence at a01d47f2, retained as a same-build comparator.
let json: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
std::fs::File::open(root.join("block-behaviours.json"))?,
))?;
BlockFacts::load_with_behaviours(root, assets.registry, &json)?
} else if mode == "shared" {
BlockFacts::load_pair(root, assets.registry)?
} else {
let parse_started = Instant::now();
let json: serde_json::Value = serde_json::from_reader(std::io::BufReader::new(
std::fs::File::open(root.join("block-behaviours.json"))?,
))?;
let parse_ms = parse_started.elapsed().as_secs_f64() * 1000.0;
let facts_started = Instant::now();
let (facts, gameplay) = BlockFacts::load_with_projection(root, assets.registry, &json)?;
let facts_ms = facts_started.elapsed().as_secs_f64() * 1000.0;
let behaviours_started = Instant::now();
let behaviours = Behaviours::load_with_gameplay(assets.registry, root, &json, &gameplay)?;
let behaviours_ms = behaviours_started.elapsed().as_secs_f64() * 1000.0;
subphases = Some((parse_ms, facts_ms, behaviours_ms));
if mode == "deferred-held" {
retained_inputs = Some((json, gameplay));
}
(facts, behaviours)
};
println!("phase_ms={:.3}", started.elapsed().as_secs_f64() * 1000.0);
println!("total_ms={:.3}", total.elapsed().as_secs_f64() * 1000.0);
println!("states={}", facts.states.len());
if let Some((parse, facts, behaviours)) = subphases {
println!("behaviour_json_ms={parse:.3}");
println!("facts_ms={facts:.3}");
println!("behaviours_ms={behaviours:.3}");
}
if let Some(rss) = resident_kib() {
println!("post_rss_kib={rss}");
}
std::hint::black_box((facts, behaviours, retained_inputs));
Ok(())
}
Loaded 100 of 1284 files, more files were not shown because too many files have changed in this diff. Show more