mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
850354ca28 | ||
|
|
4fe9d19ca3 | ||
|
|
213cffcf3e | ||
|
|
75d818fad6 | ||
|
|
a57efd4fa0 | ||
|
|
828398b5ec | ||
|
|
43a6fe0f7d | ||
|
|
91dc5b0f18 | ||
|
|
33dcc66379 | ||
|
|
35ffbebcdd |
No files matched your search
Generated
+1
@@ -1395,6 +1395,7 @@ dependencies = [
|
||||
name = "horizon-bench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"horizon-telemetry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -8,3 +8,5 @@ description = "The benchmark report format (docs/benchmark-report.md), shared by
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# The hardware probes shared with telemetry (cores, clocks, drive, VRAM, power, form).
|
||||
horizon-telemetry = { path = "../horizon-telemetry", default-features = false }
|
||||
@@ -16,8 +16,22 @@ pub struct Hardware {
|
||||
pub cpu: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_threads: Option<usize>,
|
||||
/// Physical cores.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_cores: Option<u16>,
|
||||
/// Base and top boost clock, where the OS gives them.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_base_mhz: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cpu_boost_mhz: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ram_mb: Option<u64>,
|
||||
/// The drive under the game folder: `nvme`, `ssd` or `hdd`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub storage: Option<String>,
|
||||
/// `laptop` or `desktop`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub form: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gpu: Option<Gpu>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -33,6 +47,9 @@ pub struct Gpu {
|
||||
pub backend: String,
|
||||
/// `integrated`, `discrete`, `virtual`, `cpu`, `other`.
|
||||
pub kind: String,
|
||||
/// Its own memory in MiB (rounded to 256), for a discrete GPU whose driver says.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub vram_mb: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -47,17 +64,50 @@ pub struct Display {
|
||||
|
||||
impl Hardware {
|
||||
pub fn detect() -> Hardware {
|
||||
let probes = probes();
|
||||
Hardware {
|
||||
os: std::env::consts::OS.to_owned(),
|
||||
os_version: os_version(),
|
||||
arch: std::env::consts::ARCH.to_owned(),
|
||||
cpu: cpu(),
|
||||
cpu_threads: std::thread::available_parallelism().ok().map(|n| n.get()),
|
||||
cpu_cores: probes.cores,
|
||||
cpu_base_mhz: probes.base,
|
||||
cpu_boost_mhz: probes.boost,
|
||||
ram_mb: ram_bytes().map(|b| b >> 20),
|
||||
storage: None,
|
||||
form: probes.form,
|
||||
gpu: None,
|
||||
display: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Hardware::detect`] with the kind of drive `game_dir` is on.
|
||||
pub fn detect_in(game_dir: &std::path::Path) -> Hardware {
|
||||
Hardware {
|
||||
storage: horizon_telemetry::system::storage(game_dir).map(str::to_owned),
|
||||
..Hardware::detect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the telemetry probes read (`horizon_telemetry::system`).
|
||||
struct Probes {
|
||||
cores: Option<u16>,
|
||||
base: Option<u32>,
|
||||
boost: Option<u32>,
|
||||
form: Option<String>,
|
||||
}
|
||||
|
||||
fn probes() -> Probes {
|
||||
let s = horizon_telemetry::payload::System::detect();
|
||||
let some = |n: u32| (n > 0).then_some(n);
|
||||
Probes {
|
||||
cores: (s.cores > 0).then_some(s.cores),
|
||||
base: some(s.cpu_base_mhz),
|
||||
boost: some(s.cpu_boost_mhz),
|
||||
form: (!s.form.is_empty()).then_some(s.form),
|
||||
}
|
||||
}
|
||||
|
||||
/// The one-minute load average: above about the number of threads the machine was busy.
|
||||
@@ -82,25 +132,7 @@ pub fn load_average() -> Option<f64> {
|
||||
|
||||
/// `ac` or `battery`; `None` where it can't be read (and on machines without a battery on Linux).
|
||||
pub fn power_source() -> Option<&'static str> {
|
||||
if cfg!(target_os = "macos") {
|
||||
let batt = output("pmset", &["-g", "batt"])?;
|
||||
return Some(if batt.contains("'Battery Power'") {
|
||||
"battery"
|
||||
} else {
|
||||
"ac"
|
||||
});
|
||||
}
|
||||
let mut on_battery = None;
|
||||
for entry in std::fs::read_dir("/sys/class/power_supply").ok()?.flatten() {
|
||||
let read =
|
||||
|name: &str| std::fs::read_to_string(entry.path().join(name)).unwrap_or_default();
|
||||
match read("type").trim() {
|
||||
"Mains" if read("online").trim() == "1" => return Some("ac"),
|
||||
"Battery" => on_battery = Some("battery"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
on_battery
|
||||
horizon_telemetry::system::power()
|
||||
}
|
||||
|
||||
/// Resident memory of a process, in bytes (macOS, Linux).
|
||||
|
||||
@@ -140,7 +140,22 @@ fn run(control: &Control, setup: Setup) -> Result<PathBuf, String> {
|
||||
driver: text(&renderer["driver"]),
|
||||
backend: text(&renderer["backend"]).to_lowercase(),
|
||||
kind: text(&renderer["device_type"]),
|
||||
vram_mb: None,
|
||||
});
|
||||
if let Some(gpu) = report.hardware.gpu.as_mut() {
|
||||
let vendor = horizon_telemetry::system::gpu_vendor(&gpu.name, 0);
|
||||
let vendor = if gpu.vendor.is_empty() {
|
||||
vendor
|
||||
} else {
|
||||
gpu.vendor.as_str()
|
||||
}
|
||||
.to_owned();
|
||||
if gpu.kind.contains("iscrete") {
|
||||
gpu.vram_mb = horizon_telemetry::system::vram_mb(&vendor);
|
||||
}
|
||||
}
|
||||
report.hardware.storage =
|
||||
horizon_telemetry::system::storage(&setup.game_dir).map(str::to_owned);
|
||||
report.hardware.display = serde_json::from_value(title["display"].clone()).ok();
|
||||
|
||||
replace_world(&setup.game_dir)?;
|
||||
@@ -224,6 +239,10 @@ fn run(control: &Control, setup: Setup) -> Result<PathBuf, String> {
|
||||
|
||||
let status = control.status();
|
||||
let renderer = &status["renderer"];
|
||||
// On Wayland the window is on a screen only once it has been shown: ask again.
|
||||
if report.hardware.display.is_none() {
|
||||
report.hardware.display = serde_json::from_value(status["display"].clone()).ok();
|
||||
}
|
||||
let c = &mut report.conditions;
|
||||
c.scenario = Some(setup.scenario.clone());
|
||||
c.seed = Some(SEED.to_string());
|
||||
|
||||
@@ -275,10 +275,9 @@ struct Options {
|
||||
benchmark: Option<String>,
|
||||
/// `--dev-mod PATH` (repeatable): mods always on and reloaded when rebuilt.
|
||||
dev_mods: Vec<PathBuf>,
|
||||
/// `--server-thread` or `HORIZON_SERVER_THREAD=1`: single player runs on an
|
||||
/// integrated server thread (game/integrated.rs), as vanilla's does. Off by
|
||||
/// default again until a server's players can board, ride and hit
|
||||
/// vehicles (#1222); `--no-server-thread` keeps it off.
|
||||
/// Single player runs on an integrated server thread (game/integrated.rs),
|
||||
/// as vanilla's does: the default. `--no-server-thread` or
|
||||
/// `HORIZON_SERVER_THREAD=0` runs the world in the frame instead.
|
||||
server_thread: bool,
|
||||
/// Set on the integrated server's own options: it draws nothing, so it skips meshing.
|
||||
integrated: bool,
|
||||
@@ -335,7 +334,7 @@ impl Options {
|
||||
dev_mods: Vec::new(),
|
||||
// The command harness (HORIZON_COMMAND_SCRIPT) drives the world
|
||||
// itself, without the event loop: it stays in the frame.
|
||||
server_thread: std::env::var("HORIZON_SERVER_THREAD").is_ok_and(|v| v == "1")
|
||||
server_thread: std::env::var("HORIZON_SERVER_THREAD").map_or(true, |v| v != "0")
|
||||
&& std::env::var_os("HORIZON_COMMAND_SCRIPT").is_none(),
|
||||
integrated: false,
|
||||
};
|
||||
@@ -345,7 +344,7 @@ impl Options {
|
||||
match arg.as_str() {
|
||||
"--help" | "-h" => {
|
||||
println!(
|
||||
"Horizon — Minecraft Java Edition {} in Rust\n\n --resources PATH Prepared resources (horizon-prepare)\n --render-distance N Chunks, 2..64 (default 12)\n --world NAME World folder under saves/ (default \"New World\")\n --seed N World seed for new worlds (default 0)\n --creative Creative mode (default survival)\n --test-terrain Simple test terrain instead of vanilla generation\n --frames N Exit after N frames (testing)\n --world-frames N Exit after N frames drawn in the world (testing)\n --smoke-report PATH Write a JSON report on exit\n --benchmark SCENARIO Run a benchmark (standard, village, forest, ocean, mountain), write its report to benchmarks/ and quit\n --control Accept horizon-ctl commands on a local socket\n --control-socket PATH Control socket path (default $XDG_RUNTIME_DIR/horizon-control.sock)\n --server HOST[:PORT] Join a multiplayer server (offline mode) at start\n --name NAME Player name for multiplayer (default: generated once, kept in horizon-player.txt)\n --session-file PATH Signed-in account from the launcher (read once, then deleted)\n --dev-mod PATH A Horizon mod (.hmod or folder) to develop: always on, reloaded when rebuilt\n --server-thread Run single player's world on its server thread (no vehicles yet)\n\nControls: mouse look, WASD move, Space jump, Left Shift sneak, Left Ctrl sprint,\nleft click break, right click place/use, 1-9/scroll hotbar, E inventory,\nQ drop, F3 debug, Esc pause.",
|
||||
"Horizon — Minecraft Java Edition {} in Rust\n\n --resources PATH Prepared resources (horizon-prepare)\n --render-distance N Chunks, 2..64 (default 12)\n --world NAME World folder under saves/ (default \"New World\")\n --seed N World seed for new worlds (default 0)\n --creative Creative mode (default survival)\n --test-terrain Simple test terrain instead of vanilla generation\n --frames N Exit after N frames (testing)\n --world-frames N Exit after N frames drawn in the world (testing)\n --smoke-report PATH Write a JSON report on exit\n --benchmark SCENARIO Run a benchmark (standard, village, forest, ocean, mountain), write its report to benchmarks/ and quit\n --control Accept horizon-ctl commands on a local socket\n --control-socket PATH Control socket path (default $XDG_RUNTIME_DIR/horizon-control.sock)\n --server HOST[:PORT] Join a multiplayer server (offline mode) at start\n --name NAME Player name for multiplayer (default: generated once, kept in horizon-player.txt)\n --session-file PATH Signed-in account from the launcher (read once, then deleted)\n --dev-mod PATH A Horizon mod (.hmod or folder) to develop: always on, reloaded when rebuilt\n --no-server-thread Run single player's world in the frame, not on its server thread\n\nControls: mouse look, WASD move, Space jump, Left Shift sneak, Left Ctrl sprint,\nleft click break, right click place/use, 1-9/scroll hotbar, E inventory,\nQ drop, F3 debug, Esc pause.",
|
||||
horizon_core::VANILLA_VERSION
|
||||
);
|
||||
return Ok(None);
|
||||
@@ -7717,6 +7716,10 @@ impl Game<'_> {
|
||||
fullscreen: s.fullscreen,
|
||||
graphics: s.graphics_preset.clone(),
|
||||
};
|
||||
// Again now: on Wayland the window is on a screen only once it has been shown.
|
||||
if let Some(window) = &self.window {
|
||||
horizon_client::telemetry::note_display(window);
|
||||
}
|
||||
self.perf
|
||||
.finish(&horizon_client::logging::game_home(), settings);
|
||||
}
|
||||
@@ -10515,6 +10518,7 @@ impl ApplicationHandler for Game<'_> {
|
||||
return;
|
||||
}
|
||||
};
|
||||
horizon_client::telemetry::note_display(&window);
|
||||
match self.new_renderer(window.clone()) {
|
||||
Ok(r) => {
|
||||
self.renderer = Some(r);
|
||||
@@ -12606,6 +12610,7 @@ impl Loading {
|
||||
return;
|
||||
}
|
||||
};
|
||||
horizon_client::telemetry::note_display(&window);
|
||||
let offscreen = self.options.as_ref().is_some_and(|o| o.offscreen);
|
||||
match loading_renderer(
|
||||
window.clone(),
|
||||
|
||||
@@ -906,7 +906,7 @@ impl Game<'_> {
|
||||
"streaming": self.streaming.stages().into_iter().map(|(k, v)| (k.to_string(), v.into())).collect::<serde_json::Map<_, _>>(),
|
||||
"dirty_light": self.world.dirty_light.len(),
|
||||
"renderer": renderer,
|
||||
"display": self.window.as_ref().and_then(|w| w.current_monitor()).map(|m| json!({
|
||||
"display": self.window.as_deref().and_then(horizon_client::telemetry::monitor).map(|m| json!({
|
||||
"width": m.size().width,
|
||||
"height": m.size().height,
|
||||
"scale": m.scale_factor(),
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
//! the world's options) with [`Integrated::call`], as vanilla's client calls
|
||||
//! into its `IntegratedServer`.
|
||||
//!
|
||||
//! Opt-in again (`--server-thread` or `HORIZON_SERVER_THREAD=1`): it was the
|
||||
//! default for one day, but a server's players can't board, ride or hit
|
||||
//! boats and minecarts yet (#1222), and single player's window is one of
|
||||
//! them here.
|
||||
//! On by default. `--no-server-thread` or `HORIZON_SERVER_THREAD=0` runs
|
||||
//! the world in the frame instead (the escape hatch while the network
|
||||
//! player's side catches up with the in-frame one).
|
||||
use super::{
|
||||
Game, Options, PackResources, Resources, UiResources,
|
||||
lan::{LanSettings, Owner},
|
||||
|
||||
@@ -694,13 +694,15 @@ fn describe_adapter(adapter: &wgpu::Adapter) {
|
||||
info.backend,
|
||||
info.device_type
|
||||
);
|
||||
crate::telemetry::set_gpu(
|
||||
&info.name,
|
||||
format!("{} {}", info.driver, info.driver_info)
|
||||
crate::telemetry::set_gpu(crate::telemetry::Gpu {
|
||||
name: info.name.clone(),
|
||||
vendor: horizon_telemetry::system::gpu_vendor(&info.name, info.vendor).to_owned(),
|
||||
kind: crate::telemetry::gpu_kind(&format!("{:?}", info.device_type)).to_owned(),
|
||||
driver: format!("{} {}", info.driver, info.driver_info)
|
||||
.trim()
|
||||
.to_owned(),
|
||||
format!("{:?}", info.backend),
|
||||
);
|
||||
backend: format!("{:?}", info.backend),
|
||||
});
|
||||
crate::crash::set_detail(
|
||||
"Graphics Adapter",
|
||||
format!(
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
|
||||
use horizon_telemetry::payload::{Crash, Perf, Settings, System, Timings};
|
||||
use horizon_telemetry::payload::{Crash, Display, Perf, Settings, System, Timings};
|
||||
use horizon_telemetry::scrub::Scrubber;
|
||||
use horizon_telemetry::{Item, Payload, Source, outbox};
|
||||
|
||||
@@ -25,8 +25,21 @@ static STARTED: OnceLock<Instant> = OnceLock::new();
|
||||
static UI_READY_MS: AtomicU32 = AtomicU32::new(u32::MAX);
|
||||
static CORE_READY_MS: AtomicU32 = AtomicU32::new(u32::MAX);
|
||||
static REST_READY_MS: AtomicU32 = AtomicU32::new(u32::MAX);
|
||||
/// The GPU's name, driver and backend, once the renderer has its adapter.
|
||||
static GPU: Mutex<Option<(String, String, String)>> = Mutex::new(None);
|
||||
/// The GPU, once the renderer has its adapter: name, maker, kind, driver, backend, its own memory.
|
||||
static GPU: Mutex<Option<Gpu>> = Mutex::new(None);
|
||||
/// The screen the window opened on.
|
||||
static DISPLAY: Mutex<Option<Display>> = Mutex::new(None);
|
||||
|
||||
/// The adapter as the renderer found it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Gpu {
|
||||
pub name: String,
|
||||
pub vendor: String,
|
||||
/// `integrated`, `discrete`, `virtual`, `cpu` or `other`.
|
||||
pub kind: String,
|
||||
pub driver: String,
|
||||
pub backend: String,
|
||||
}
|
||||
|
||||
/// When the game started, for the time to the title screen. Call first thing in `main`.
|
||||
pub fn mark_start() {
|
||||
@@ -61,18 +74,73 @@ fn readiness_ms(mark: &AtomicU32) -> Option<u32> {
|
||||
(ms != u32::MAX).then_some(ms)
|
||||
}
|
||||
|
||||
pub fn set_gpu(name: &str, driver: String, backend: String) {
|
||||
*GPU.lock().unwrap_or_else(|e| e.into_inner()) = Some((name.to_owned(), driver, backend));
|
||||
pub fn set_gpu(gpu: Gpu) {
|
||||
*GPU.lock().unwrap_or_else(|e| e.into_inner()) = Some(gpu);
|
||||
}
|
||||
|
||||
/// This machine, with the GPU once the renderer knows it.
|
||||
pub fn system() -> System {
|
||||
let mut system = System::detect();
|
||||
if let Some((gpu, driver, backend)) = GPU.lock().unwrap_or_else(|e| e.into_inner()).clone() {
|
||||
system.gpu = gpu;
|
||||
system.driver = driver;
|
||||
system.backend = backend;
|
||||
/// The screen the window is on: size in physical pixels, refresh rate, scale.
|
||||
pub fn set_display(width: u32, height: u32, refresh_hz: u32, scale: f64) {
|
||||
*DISPLAY.lock().unwrap_or_else(|e| e.into_inner()) = Some(Display {
|
||||
width,
|
||||
height,
|
||||
refresh_hz,
|
||||
scale: scale as f32,
|
||||
});
|
||||
}
|
||||
|
||||
/// The screen `window` is on, else the primary one, else the only one: right after the window
|
||||
/// is made, Wayland doesn't say yet which screen it is on.
|
||||
pub fn monitor(window: &winit::window::Window) -> Option<winit::monitor::MonitorHandle> {
|
||||
let only = || {
|
||||
let mut all = window.available_monitors();
|
||||
all.next().filter(|_| all.next().is_none())
|
||||
};
|
||||
window
|
||||
.current_monitor()
|
||||
.or_else(|| window.primary_monitor())
|
||||
.or_else(only)
|
||||
}
|
||||
|
||||
/// Keeps [`monitor`]'s size, refresh rate and scale for the items' `system.display`.
|
||||
pub fn note_display(window: &winit::window::Window) {
|
||||
if let Some(m) = monitor(window) {
|
||||
set_display(
|
||||
m.size().width,
|
||||
m.size().height,
|
||||
m.refresh_rate_millihertz().map_or(0, |r| r.div_ceil(1000)),
|
||||
m.scale_factor(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// wgpu's device type in the words items use.
|
||||
pub fn gpu_kind(device_type: &str) -> &'static str {
|
||||
match device_type {
|
||||
"IntegratedGpu" => "integrated",
|
||||
"DiscreteGpu" => "discrete",
|
||||
"VirtualGpu" => "virtual",
|
||||
"Cpu" => "cpu",
|
||||
_ => "other",
|
||||
}
|
||||
}
|
||||
|
||||
/// This machine, with the GPU and the screen once the renderer knows them. The drive is the one
|
||||
/// under the game folder (the current folder).
|
||||
pub fn system() -> System {
|
||||
let home = std::env::current_dir().ok();
|
||||
let mut system = System::detect_in(home.as_deref());
|
||||
if let Some(gpu) = GPU.lock().unwrap_or_else(|e| e.into_inner()).clone() {
|
||||
system.vram_mb = match gpu.kind.as_str() {
|
||||
"discrete" => horizon_telemetry::system::vram_mb(&gpu.vendor).unwrap_or(0),
|
||||
_ => 0,
|
||||
};
|
||||
system.gpu = gpu.name;
|
||||
system.gpu_vendor = gpu.vendor;
|
||||
system.gpu_kind = gpu.kind;
|
||||
system.driver = gpu.driver;
|
||||
system.backend = gpu.backend;
|
||||
}
|
||||
system.display = DISPLAY.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
system
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//! The hardware classes the public output breaks results down by. Coarse on purpose: a class
|
||||
//! holds many machines, and a published cell still needs `MIN_INSTALLS` different installs.
|
||||
//! `website/scripts/benchmarks.mjs` has the same functions, tested on the same fixtures.
|
||||
//!
|
||||
//! The raw values (the exact memory, the driver, the screen) stay in the database for the
|
||||
//! retention period and are never published.
|
||||
use horizon_telemetry::payload::System;
|
||||
use serde_json::Value;
|
||||
|
||||
/// The dimensions, in the order [`of_report`] and [`of_system`] return them.
|
||||
pub const DIMENSIONS: [&str; 5] = ["ram", "vram", "storage", "os", "gpu_kind"];
|
||||
|
||||
pub type Classes = [Option<&'static str>; 5];
|
||||
|
||||
/// Installed memory in GB (a little under the size on the box: some is reserved).
|
||||
pub fn ram(gb: f64) -> Option<&'static str> {
|
||||
Some(match gb {
|
||||
g if g.is_nan() || g <= 0.0 => return None,
|
||||
g if g <= 5.0 => "4 GB or less",
|
||||
g if g <= 10.0 => "8 GB",
|
||||
g if g <= 20.0 => "16 GB",
|
||||
g if g <= 40.0 => "32 GB",
|
||||
_ => "64 GB or more",
|
||||
})
|
||||
}
|
||||
|
||||
/// A GPU's own memory in MiB; an integrated GPU shares the system's.
|
||||
pub fn vram(mb: f64, kind: &str) -> Option<&'static str> {
|
||||
if gpu_kind(kind) == Some("integrated") {
|
||||
return Some("shared");
|
||||
}
|
||||
Some(match mb {
|
||||
m if m.is_nan() || m <= 0.0 => return None,
|
||||
m if m <= 2560.0 => "2 GB or less",
|
||||
m if m <= 4608.0 => "4 GB",
|
||||
m if m <= 6656.0 => "6 GB",
|
||||
m if m <= 8704.0 => "8 GB",
|
||||
m if m <= 12800.0 => "12 GB",
|
||||
m if m <= 16896.0 => "16 GB",
|
||||
_ => "more than 16 GB",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn storage(s: &str) -> Option<&'static str> {
|
||||
["nvme", "ssd", "hdd"].into_iter().find(|k| *k == s)
|
||||
}
|
||||
|
||||
pub fn os(s: &str) -> Option<&'static str> {
|
||||
["linux", "macos", "windows"].into_iter().find(|k| *k == s)
|
||||
}
|
||||
|
||||
/// `integrated` or `discrete`, from either wording (`IntegratedGpu`, as wgpu says it, too).
|
||||
pub fn gpu_kind(s: &str) -> Option<&'static str> {
|
||||
match s {
|
||||
"integrated" | "IntegratedGpu" => Some("integrated"),
|
||||
"discrete" | "DiscreteGpu" => Some("discrete"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A benchmark report's classes (`docs/benchmark-report.md`'s `hardware`).
|
||||
pub fn of_report(report: &Value) -> Classes {
|
||||
let hw = &report["hardware"];
|
||||
let kind = hw["gpu"]["kind"].as_str().unwrap_or("");
|
||||
[
|
||||
hw["ram_mb"].as_f64().and_then(|mb| ram(mb / 1024.0)),
|
||||
vram(hw["gpu"]["vram_mb"].as_f64().unwrap_or(0.0), kind),
|
||||
storage(hw["storage"].as_str().unwrap_or("")),
|
||||
os(hw["os"].as_str().unwrap_or("")),
|
||||
gpu_kind(kind),
|
||||
]
|
||||
}
|
||||
|
||||
/// A telemetry item's machine's classes.
|
||||
pub fn of_system(s: &System) -> Classes {
|
||||
[
|
||||
ram(f64::from(s.ram_gb)),
|
||||
vram(f64::from(s.vram_mb), &s.gpu_kind),
|
||||
storage(&s.storage),
|
||||
os(&s.os),
|
||||
gpu_kind(&s.gpu_kind),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn classes_are_coarse() {
|
||||
assert_eq!(ram(15.6), Some("16 GB"));
|
||||
assert_eq!(ram(31.0), Some("32 GB"));
|
||||
assert_eq!(ram(64.0), Some("64 GB or more"));
|
||||
assert_eq!(ram(0.0), None);
|
||||
assert_eq!(vram(16376.0, "discrete"), Some("16 GB"));
|
||||
assert_eq!(vram(8192.0, "DiscreteGpu"), Some("8 GB"));
|
||||
assert_eq!(vram(0.0, "integrated"), Some("shared"));
|
||||
assert_eq!(vram(0.0, ""), None);
|
||||
assert_eq!(storage("nvme"), Some("nvme"));
|
||||
assert_eq!(storage("tape"), None);
|
||||
assert_eq!(gpu_kind("virtual"), None);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{delete, get, post};
|
||||
use horizon_telemetry::batch;
|
||||
|
||||
pub mod classes;
|
||||
pub mod live;
|
||||
pub mod store;
|
||||
pub mod summary;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The database: one SQLite file, kept small.
|
||||
//!
|
||||
//! - `systems`: each distinct machine description once. Rows point at it, so a CPU and GPU
|
||||
//! name isn't stored again with every session.
|
||||
//! name isn't stored again with every session. `extra` holds the later hardware fields
|
||||
//! (cores, clocks, GPU maker and kind, VRAM, drive, screen, power, form) as JSON.
|
||||
//! - `perf`: one row per play session, in columns. A frame or tick summary is an 80-byte blob
|
||||
//! ([`pack`]), not JSON.
|
||||
//! - `reports`: crash and bug reports and benchmark uploads, their `data` zstd-compressed.
|
||||
@@ -17,6 +18,7 @@ use horizon_telemetry::Kind;
|
||||
use horizon_telemetry::batch::Batch;
|
||||
use horizon_telemetry::payload::{BUCKET_EDGES_MS, Payload, Summary, System};
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const BUCKETS: usize = BUCKET_EDGES_MS.len() + 1;
|
||||
|
||||
@@ -27,8 +29,8 @@ CREATE TABLE IF NOT EXISTS systems (
|
||||
id INTEGER PRIMARY KEY,
|
||||
os TEXT NOT NULL, os_version TEXT NOT NULL, arch TEXT NOT NULL, cpu TEXT NOT NULL,
|
||||
threads INTEGER NOT NULL, ram_gb INTEGER NOT NULL,
|
||||
gpu TEXT NOT NULL, driver TEXT NOT NULL, backend TEXT NOT NULL,
|
||||
UNIQUE (os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend)
|
||||
gpu TEXT NOT NULL, driver TEXT NOT NULL, backend TEXT NOT NULL, extra TEXT NOT NULL DEFAULT '{}',
|
||||
UNIQUE (os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend, extra)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS perf (
|
||||
install BLOB NOT NULL, day INTEGER NOT NULL, version TEXT NOT NULL, system INTEGER NOT NULL,
|
||||
@@ -126,8 +128,9 @@ pub struct Store {
|
||||
|
||||
impl Store {
|
||||
pub fn open(path: &str) -> rusqlite::Result<Self> {
|
||||
let db = Connection::open(path)?;
|
||||
let mut db = Connection::open(path)?;
|
||||
db.execute_batch(SCHEMA)?;
|
||||
migrate(&mut db)?;
|
||||
Ok(Self { db })
|
||||
}
|
||||
|
||||
@@ -357,14 +360,91 @@ impl Store {
|
||||
})?
|
||||
.collect::<Result<_, _>>()?
|
||||
};
|
||||
let breakdowns = self.breakdowns(from, to)?;
|
||||
Ok(serde_json::json!({
|
||||
"from_day": from, "to_day": to, "min_install_days": k,
|
||||
"bucket_edges_ms": BUCKET_EDGES_MS.map(|e| (f64::from(e) * 10.0).round() / 10.0),
|
||||
"totals": { "sessions": sessions, "hours": (hours * 10.0).round() / 10.0 },
|
||||
"groups": out, "crashes_by_version": crashes,
|
||||
"groups": out, "crashes_by_version": crashes, "breakdowns": breakdowns,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Play sessions in `from..=to` per hardware class (`classes`): for each RAM, VRAM, drive,
|
||||
/// OS and GPU kind class with at least `MIN_INSTALLS` different installs, its installs,
|
||||
/// sessions, hours and mean p50 and p99 frame times. From the raw rows, so only within the
|
||||
/// retention period.
|
||||
fn breakdowns(&self, from: u32, to: u32) -> rusqlite::Result<serde_json::Value> {
|
||||
#[derive(Default)]
|
||||
struct Cell {
|
||||
installs: HashSet<Vec<u8>>,
|
||||
sessions: u64,
|
||||
seconds: u64,
|
||||
p50_sum: f64,
|
||||
p99_sum: f64,
|
||||
}
|
||||
let mut cells: [HashMap<&'static str, Cell>; 5] = Default::default();
|
||||
let mut q = self.db.prepare(
|
||||
"SELECT perf.install, perf.seconds, perf.frames, systems.os, systems.ram_gb, systems.extra
|
||||
FROM perf JOIN systems ON systems.id = perf.system WHERE perf.day BETWEEN ?1 AND ?2",
|
||||
)?;
|
||||
let mut rows = q.query([from, to])?;
|
||||
while let Some(r) = rows.next()? {
|
||||
let Some(frames) = unpack(&r.get::<_, Vec<u8>>(2)?) else {
|
||||
continue;
|
||||
};
|
||||
let extra: Extra = serde_json::from_str(&r.get::<_, String>(5)?).unwrap_or_default();
|
||||
let system = System {
|
||||
os: r.get(3)?,
|
||||
ram_gb: r.get(4)?,
|
||||
gpu_kind: extra.gpu_kind,
|
||||
vram_mb: extra.vram_mb,
|
||||
storage: extra.storage,
|
||||
..System::default()
|
||||
};
|
||||
let install: Vec<u8> = r.get(0)?;
|
||||
let seconds: u64 = r.get(1)?;
|
||||
for (dim, class) in crate::classes::of_system(&system).into_iter().enumerate() {
|
||||
let Some(class) = class else { continue };
|
||||
let c = cells[dim].entry(class).or_default();
|
||||
c.installs.insert(install.clone());
|
||||
c.sessions += 1;
|
||||
c.seconds += seconds;
|
||||
c.p50_sum += f64::from(frames.p50_ms);
|
||||
c.p99_sum += f64::from(frames.p99_ms);
|
||||
}
|
||||
}
|
||||
let mut out = serde_json::Map::new();
|
||||
for (dim, by_class) in crate::classes::DIMENSIONS.iter().zip(cells) {
|
||||
let mut list: Vec<(&str, Cell)> = by_class
|
||||
.into_iter()
|
||||
.filter(|(_, c)| c.installs.len() >= crate::summary::MIN_INSTALLS)
|
||||
.collect();
|
||||
list.sort_by(|a, b| {
|
||||
b.1.installs
|
||||
.len()
|
||||
.cmp(&a.1.installs.len())
|
||||
.then(a.0.cmp(b.0))
|
||||
});
|
||||
if list.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let avg = |sum: f64, n: u64| (sum / n as f64 * 10.0).round() / 10.0;
|
||||
out.insert(
|
||||
dim.to_string(),
|
||||
list.into_iter()
|
||||
.map(|(class, c)| {
|
||||
serde_json::json!({
|
||||
"class": class, "installs": c.installs.len(), "sessions": c.sessions,
|
||||
"hours": (c.seconds as f64 / 360.0).round() / 10.0,
|
||||
"p50_ms": avg(c.p50_sum, c.sessions), "p99_ms": avg(c.p99_sum, c.sessions),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Ok(serde_json::Value::Object(out))
|
||||
}
|
||||
|
||||
/// Every benchmark upload received since `from_day`, with its install (to count installs, never shown)
|
||||
/// and release, for `summary`.
|
||||
pub fn benchmark_uploads(
|
||||
@@ -398,15 +478,74 @@ impl Store {
|
||||
}
|
||||
}
|
||||
|
||||
/// The hardware fields after the first nine, as `systems.extra` keeps them.
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Extra {
|
||||
pub cores: u16,
|
||||
pub cpu_base_mhz: u32,
|
||||
pub cpu_boost_mhz: u32,
|
||||
pub gpu_vendor: String,
|
||||
pub gpu_kind: String,
|
||||
pub vram_mb: u32,
|
||||
pub storage: String,
|
||||
pub display: Option<horizon_telemetry::payload::Display>,
|
||||
pub power: String,
|
||||
pub form: String,
|
||||
}
|
||||
|
||||
impl Extra {
|
||||
fn of(s: &System) -> Self {
|
||||
Self {
|
||||
cores: s.cores,
|
||||
cpu_base_mhz: s.cpu_base_mhz,
|
||||
cpu_boost_mhz: s.cpu_boost_mhz,
|
||||
gpu_vendor: s.gpu_vendor.clone(),
|
||||
gpu_kind: s.gpu_kind.clone(),
|
||||
vram_mb: s.vram_mb,
|
||||
storage: s.storage.clone(),
|
||||
display: s.display.clone(),
|
||||
power: s.power.clone(),
|
||||
form: s.form.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Databases from before `systems.extra`: the table rebuilt with it (ids kept), once.
|
||||
fn migrate(db: &mut Connection) -> rusqlite::Result<()> {
|
||||
let has_extra = db
|
||||
.prepare("SELECT 1 FROM pragma_table_info('systems') WHERE name = 'extra'")?
|
||||
.exists([])?;
|
||||
if has_extra {
|
||||
return Ok(());
|
||||
}
|
||||
let tx = db.transaction()?;
|
||||
tx.execute_batch(
|
||||
"CREATE TABLE systems_new (
|
||||
id INTEGER PRIMARY KEY,
|
||||
os TEXT NOT NULL, os_version TEXT NOT NULL, arch TEXT NOT NULL, cpu TEXT NOT NULL,
|
||||
threads INTEGER NOT NULL, ram_gb INTEGER NOT NULL,
|
||||
gpu TEXT NOT NULL, driver TEXT NOT NULL, backend TEXT NOT NULL, extra TEXT NOT NULL DEFAULT '{}',
|
||||
UNIQUE (os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend, extra)
|
||||
);
|
||||
INSERT INTO systems_new (id, os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend)
|
||||
SELECT id, os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend FROM systems;
|
||||
DROP TABLE systems;
|
||||
ALTER TABLE systems_new RENAME TO systems;",
|
||||
)?;
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn system_id(tx: &rusqlite::Transaction, s: &System) -> rusqlite::Result<i64> {
|
||||
let extra = serde_json::to_string(&Extra::of(s)).unwrap_or_else(|_| "{}".into());
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO systems (os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![s.os, s.os_version, s.arch, s.cpu, s.threads, s.ram_gb, s.gpu, s.driver, s.backend],
|
||||
"INSERT OR IGNORE INTO systems (os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend, extra)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
params![s.os, s.os_version, s.arch, s.cpu, s.threads, s.ram_gb, s.gpu, s.driver, s.backend, extra],
|
||||
)?;
|
||||
tx.query_row(
|
||||
"SELECT id FROM systems WHERE os = ?1 AND os_version = ?2 AND arch = ?3 AND cpu = ?4
|
||||
AND threads = ?5 AND ram_gb = ?6 AND gpu = ?7 AND driver = ?8 AND backend = ?9",
|
||||
AND threads = ?5 AND ram_gb = ?6 AND gpu = ?7 AND driver = ?8 AND backend = ?9 AND extra = ?10",
|
||||
params![
|
||||
s.os,
|
||||
s.os_version,
|
||||
@@ -416,7 +555,8 @@ fn system_id(tx: &rusqlite::Transaction, s: &System) -> rusqlite::Result<i64> {
|
||||
s.ram_gb,
|
||||
s.gpu,
|
||||
s.driver,
|
||||
s.backend
|
||||
s.backend,
|
||||
extra
|
||||
],
|
||||
|r| r.get(0),
|
||||
)
|
||||
@@ -432,3 +572,65 @@ pub fn inflate(body: &[u8]) -> serde_json::Value {
|
||||
.and_then(|_| serde_json::from_slice(&json).ok())
|
||||
.unwrap_or(serde_json::Value::Null)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A database from before `systems.extra` opens, keeps its machines' ids and takes new ones.
|
||||
#[test]
|
||||
fn systems_from_before_extra_are_kept() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"horizon-telemetry-migrate-{}.sqlite",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
{
|
||||
let db = Connection::open(&path).unwrap();
|
||||
db.execute_batch(
|
||||
"CREATE TABLE systems (
|
||||
id INTEGER PRIMARY KEY,
|
||||
os TEXT NOT NULL, os_version TEXT NOT NULL, arch TEXT NOT NULL, cpu TEXT NOT NULL,
|
||||
threads INTEGER NOT NULL, ram_gb INTEGER NOT NULL,
|
||||
gpu TEXT NOT NULL, driver TEXT NOT NULL, backend TEXT NOT NULL,
|
||||
UNIQUE (os, os_version, arch, cpu, threads, ram_gb, gpu, driver, backend)
|
||||
);
|
||||
INSERT INTO systems VALUES (7, 'linux', 'Fedora 43', 'x86_64', 'Ryzen', 32, 32, 'RTX 4070', '570', 'Vulkan');",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let path_str = path.to_str().unwrap();
|
||||
let mut store = Store::open(path_str).unwrap();
|
||||
let old: (String, String) = store
|
||||
.db
|
||||
.query_row("SELECT gpu, extra FROM systems WHERE id = 7", [], |r| {
|
||||
Ok((r.get(0)?, r.get(1)?))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(old, ("RTX 4070".to_owned(), "{}".to_owned()));
|
||||
let s = System {
|
||||
os: "linux".into(),
|
||||
gpu: "RTX 4070".into(),
|
||||
vram_mb: 12288,
|
||||
gpu_kind: "discrete".into(),
|
||||
storage: "nvme".into(),
|
||||
..System::default()
|
||||
};
|
||||
let tx = store.db.transaction().unwrap();
|
||||
let id = system_id(&tx, &s).unwrap();
|
||||
assert_eq!(system_id(&tx, &s).unwrap(), id, "the same machine once");
|
||||
let extra: String = tx
|
||||
.query_row("SELECT extra FROM systems WHERE id = ?1", [id], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap();
|
||||
tx.commit().unwrap();
|
||||
let back: Extra = serde_json::from_str(&extra).unwrap();
|
||||
assert_eq!((back.vram_mb, back.storage.as_str()), (12288, "nvme"));
|
||||
drop(store);
|
||||
Store::open(path_str).unwrap(); // a second open doesn't rebuild again
|
||||
for suffix in ["", "-wal", "-shm"] {
|
||||
let _ = std::fs::remove_file(format!("{path_str}{suffix}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,9 @@ pub fn rejection(report: &Value) -> Option<&'static str> {
|
||||
if report.get("kind").and_then(Value::as_str) != Some("game") {
|
||||
return Some("not a game run");
|
||||
}
|
||||
game(&report["app"])?;
|
||||
if game(&report["app"]).is_none() {
|
||||
return Some("not Horizon or plain vanilla");
|
||||
}
|
||||
let c = &report["conditions"];
|
||||
let scenario = c.get("scenario").and_then(Value::as_str).unwrap_or("");
|
||||
if !(scenario == "standard" || scenario == "flight" || scenario.starts_with("perf_bench:")) {
|
||||
@@ -190,6 +192,8 @@ fn newest_first(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
#[derive(Default)]
|
||||
struct Group {
|
||||
installs: HashMap<Vec<u8>, Vec<[Option<f64>; 7]>>,
|
||||
/// Per dimension and class, the installs in it.
|
||||
classes: [BTreeMap<&'static str, BTreeSet<Vec<u8>>>; 5],
|
||||
reports: usize,
|
||||
versions: BTreeSet<String>,
|
||||
}
|
||||
@@ -219,6 +223,14 @@ pub fn groups(uploads: &[Upload], min_installs: usize) -> Vec<Value> {
|
||||
.entry(u.install.clone())
|
||||
.or_default()
|
||||
.push(figures(r));
|
||||
for (dim, class) in crate::classes::of_report(r).into_iter().enumerate() {
|
||||
if let Some(class) = class {
|
||||
g.classes[dim]
|
||||
.entry(class)
|
||||
.or_default()
|
||||
.insert(u.install.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out: Vec<(usize, Value)> = by
|
||||
.into_iter()
|
||||
@@ -243,11 +255,13 @@ pub fn groups(uploads: &[Upload], min_installs: usize) -> Vec<Value> {
|
||||
versions.sort_by(|a, b| newest_first(a, b));
|
||||
versions.truncate(3);
|
||||
let installs = g.installs.len();
|
||||
let breakdowns = breakdowns(&g.classes, min_installs);
|
||||
(
|
||||
installs,
|
||||
json!({
|
||||
"gpu": gpu, "cpu": cpu, "render_distance": rd, "game": game, "source": "players",
|
||||
"installs": installs, "reports": g.reports, "versions": versions, "medians": medians,
|
||||
"breakdowns": breakdowns,
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -257,6 +271,33 @@ pub fn groups(uploads: &[Upload], min_installs: usize) -> Vec<Value> {
|
||||
out.into_iter().map(|(_, v)| v).collect()
|
||||
}
|
||||
|
||||
/// Per dimension, the classes with at least `min_installs` installs, most first:
|
||||
/// `{"ram": [{"class": "32 GB", "installs": 3}], …}`. A dimension with none is left out.
|
||||
pub fn breakdowns(
|
||||
classes: &[BTreeMap<&'static str, BTreeSet<Vec<u8>>>; 5],
|
||||
min_installs: usize,
|
||||
) -> Value {
|
||||
let mut out = serde_json::Map::new();
|
||||
for (dim, by_class) in crate::classes::DIMENSIONS.iter().zip(classes) {
|
||||
let mut cells: Vec<(&str, usize)> = by_class
|
||||
.iter()
|
||||
.map(|(c, set)| (*c, set.len()))
|
||||
.filter(|(_, n)| *n >= min_installs)
|
||||
.collect();
|
||||
cells.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
|
||||
if !cells.is_empty() {
|
||||
out.insert(
|
||||
dim.to_string(),
|
||||
cells
|
||||
.into_iter()
|
||||
.map(|(class, installs)| json!({"class": class, "installs": installs}))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::Object(out)
|
||||
}
|
||||
|
||||
/// The whole answer; `generated` is an RFC 3339 UTC time.
|
||||
pub fn summary(uploads: &[Upload], min_installs: usize, generated: &str) -> Value {
|
||||
json!({
|
||||
|
||||
@@ -157,6 +157,45 @@ async fn totals_show_only_groups_with_enough_installs() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Sessions split by hardware class; a class shows only with three installs, and never a raw value.
|
||||
#[tokio::test]
|
||||
async fn totals_break_down_by_hardware_class() {
|
||||
let s = server(Config {
|
||||
min_install_days: 1,
|
||||
..generous()
|
||||
});
|
||||
for n in 0..4 {
|
||||
let mut item = perf("NVIDIA GeForce RTX 4080", 12);
|
||||
if let Payload::Perf(p) = &mut item.payload {
|
||||
p.system.ram_gb = if n == 3 { 16 } else { 31 };
|
||||
p.system.vram_mb = 16376;
|
||||
p.system.gpu_kind = "discrete".into();
|
||||
p.system.storage = if n == 3 { "hdd" } else { "nvme" }.into();
|
||||
}
|
||||
assert_eq!(
|
||||
s.post(&install(n), vec![item]).await,
|
||||
StatusCode::NO_CONTENT
|
||||
);
|
||||
}
|
||||
let totals = s.aggregate().await;
|
||||
let b = &totals["breakdowns"];
|
||||
assert_eq!(
|
||||
b["ram"],
|
||||
serde_json::json!([{"class": "32 GB", "installs": 3, "sessions": 3, "hours": 1.5, "p50_ms": b["ram"][0]["p50_ms"], "p99_ms": b["ram"][0]["p99_ms"]}]),
|
||||
"16 GB has one install: {totals}"
|
||||
);
|
||||
assert!(b["ram"][0]["p50_ms"].as_f64().unwrap() > 8.0);
|
||||
assert_eq!(b["vram"][0]["class"], "16 GB");
|
||||
assert_eq!(b["vram"][0]["installs"], 4);
|
||||
assert_eq!(b["storage"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(b["os"][0]["class"], "linux");
|
||||
assert_eq!(b["gpu_kind"][0]["class"], "discrete");
|
||||
let text = totals.to_string();
|
||||
for raw in ["16376", "570.86", "Fedora", "hdd", &install(0)] {
|
||||
assert!(!text.contains(raw), "{raw} in {text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bad_batches_are_refused() {
|
||||
let s = server(generous());
|
||||
|
||||
@@ -148,6 +148,7 @@ async fn uploads_come_back_grouped_and_anonymous() {
|
||||
assert_eq!(origin.as_deref(), Some("https://playhorizon.dev"));
|
||||
let s: Value = serde_json::from_str(&body).unwrap();
|
||||
assert_eq!(s["groups"], expected()["groups"]);
|
||||
assert_eq!(s["groups"][0]["breakdowns"]["vram"][0]["installs"], 3);
|
||||
let generated = s["generated"].as_str().unwrap();
|
||||
assert!(
|
||||
generated.len() == 20 && generated.ends_with('Z'),
|
||||
@@ -160,6 +161,9 @@ async fn uploads_come_back_grouped_and_anonymous() {
|
||||
"secret-driver",
|
||||
"20394",
|
||||
"ram_mb",
|
||||
"vram_mb",
|
||||
"16376",
|
||||
"32768",
|
||||
] {
|
||||
assert!(!body.contains(secret), "{secret} in {body}");
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ pub use payload::{Item, Kind, Payload, Source};
|
||||
pub const SCHEMA: u32 = 1;
|
||||
|
||||
/// What players agree to. Bumped when what is collected changes: the launcher then asks again,
|
||||
/// and until the player answers nothing new is kept or sent.
|
||||
pub const CONSENT_VERSION: u32 = 1;
|
||||
/// and until the player answers nothing new is kept or sent. 2: the machine's full hardware
|
||||
/// (cores, clocks, VRAM, drive, screen, power, form), 2026-10-23.
|
||||
pub const CONSENT_VERSION: u32 = 2;
|
||||
|
||||
/// The ingest server (`crates/horizon-telemetry-server`, on Railway). `HORIZON_TELEMETRY_URL`
|
||||
/// overrides it (a local server); `HORIZON_TELEMETRY_URL=off` sends nothing at all, no ping
|
||||
|
||||
@@ -129,8 +129,9 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
/// The machine, as coarse as is useful: no serial numbers, names or exact memory.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// The machine, as coarse as is useful: no serial numbers, names or exact memory. Every field
|
||||
/// explains performance; `docs/telemetry.md` lists them. Empty or zero: not known.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct System {
|
||||
/// `linux`, `macos` or `windows`.
|
||||
@@ -140,12 +141,42 @@ pub struct System {
|
||||
pub arch: String,
|
||||
pub cpu: String,
|
||||
pub threads: u16,
|
||||
/// Physical cores.
|
||||
pub cores: u16,
|
||||
/// The CPU's base and top boost clock, where the OS says.
|
||||
pub cpu_base_mhz: u32,
|
||||
pub cpu_boost_mhz: u32,
|
||||
/// Installed memory, rounded to the GiB.
|
||||
pub ram_gb: u16,
|
||||
pub gpu: String,
|
||||
/// `nvidia`, `amd`, `intel`, `apple`…
|
||||
pub gpu_vendor: String,
|
||||
/// `integrated`, `discrete`, `virtual` or `cpu`.
|
||||
pub gpu_kind: String,
|
||||
/// The GPU's own memory, rounded to 256 MiB (0 for a GPU that shares the system's).
|
||||
pub vram_mb: u32,
|
||||
pub driver: String,
|
||||
/// `Metal`, `Vulkan`, `Dx12`…
|
||||
pub backend: String,
|
||||
/// The drive under the game folder: `nvme`, `ssd` or `hdd`.
|
||||
pub storage: String,
|
||||
/// The screen the game's window is on.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display: Option<Display>,
|
||||
/// `ac` or `battery`, when the game started.
|
||||
pub power: String,
|
||||
/// `laptop` or `desktop`.
|
||||
pub form: String,
|
||||
}
|
||||
|
||||
/// A screen: its size in physical pixels, refresh rate and scale.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Display {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub refresh_hz: u32,
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
/// The settings that change performance.
|
||||
@@ -190,7 +221,7 @@ pub struct Perf {
|
||||
|
||||
/// A crash, scrubbed: the error and its stack, the thread, and the details that say what the
|
||||
/// game was doing (graphics adapter, settings), never its log, world or server.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Crash {
|
||||
pub system: System,
|
||||
@@ -202,7 +233,7 @@ pub struct Crash {
|
||||
}
|
||||
|
||||
/// A bug report the player wrote, with the log only if they chose to add it after reading it.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Bug {
|
||||
pub system: System,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
//! The machine as an item describes it ([`System`]): what the OS says, read the same way on
|
||||
//! every start. The game adds the GPU, its driver and the graphics backend.
|
||||
//! every start. The game adds the GPU (its name, maker, kind, driver and backend, then
|
||||
//! [`vram_mb`] for it) and the screen.
|
||||
//!
|
||||
//! Only what explains performance: no serial number, host name, user name or path is read into
|
||||
//! an item. The slower probes (a command to run) are read once per process.
|
||||
use std::path::Path;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::payload::System;
|
||||
|
||||
fn command_output(program: &str, args: &[&str]) -> Option<String> {
|
||||
@@ -74,20 +81,422 @@ fn ram_bytes() -> Option<u64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical cores.
|
||||
fn cores() -> Option<u16> {
|
||||
if cfg!(target_os = "linux") {
|
||||
cores_from_cpuinfo(&read("/proc/cpuinfo")?)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
command_output("sysctl", &["-n", "hw.physicalcpu"])?
|
||||
.parse()
|
||||
.ok()
|
||||
} else if cfg!(windows) {
|
||||
powershell("(Get-CimInstance Win32_Processor | Measure-Object NumberOfCores -Sum).Sum")?
|
||||
.parse()
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinct (package, core) pairs; a listing without them (ARM boards) counts its processors.
|
||||
fn cores_from_cpuinfo(text: &str) -> Option<u16> {
|
||||
let mut pairs = std::collections::HashSet::new();
|
||||
let (mut package, mut processors) = (String::new(), 0u16);
|
||||
for line in text.lines() {
|
||||
let Some((key, value)) = line.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
match key.trim() {
|
||||
"processor" => processors += 1,
|
||||
"physical id" => package = value.trim().to_owned(),
|
||||
"core id" => {
|
||||
pairs.insert((package.clone(), value.trim().to_owned()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let n = if pairs.is_empty() {
|
||||
processors
|
||||
} else {
|
||||
pairs.len() as u16
|
||||
};
|
||||
(n > 0).then_some(n)
|
||||
}
|
||||
|
||||
/// The base and top boost clocks in MHz, where the OS gives them (not on Apple silicon).
|
||||
fn cpu_mhz() -> (Option<u32>, Option<u32>) {
|
||||
let khz = |path: &str| {
|
||||
read(path)
|
||||
.and_then(|t| t.trim().parse::<u32>().ok())
|
||||
.map(|k| k / 1000)
|
||||
};
|
||||
if cfg!(target_os = "linux") {
|
||||
let base = khz("/sys/devices/system/cpu/cpu0/cpufreq/base_frequency").or_else(|| {
|
||||
read("/sys/devices/system/cpu/cpu0/acpi_cppc/nominal_freq")
|
||||
.and_then(|t| t.trim().parse().ok())
|
||||
});
|
||||
(
|
||||
base,
|
||||
khz("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"),
|
||||
)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let hz = |key: &str| {
|
||||
command_output("sysctl", &["-n", key])
|
||||
.and_then(|t| t.parse::<u64>().ok())
|
||||
.map(|hz| (hz / 1_000_000) as u32)
|
||||
};
|
||||
(hz("hw.cpufrequency"), hz("hw.cpufrequency_max"))
|
||||
} else if cfg!(windows) {
|
||||
let base =
|
||||
powershell("(Get-CimInstance Win32_Processor | Select-Object -First 1).MaxClockSpeed")
|
||||
.and_then(|t| t.parse().ok());
|
||||
(base, None)
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn powershell(command: &str) -> Option<String> {
|
||||
command_output("powershell", &["-NoProfile", "-Command", command])
|
||||
}
|
||||
|
||||
/// `nvme`, `ssd` or `hdd` for the drive `path` is on, where it can be read.
|
||||
pub fn storage(path: &Path) -> Option<&'static str> {
|
||||
if cfg!(target_os = "linux") {
|
||||
let source = mount_source(&read("/proc/self/mountinfo")?, path)?;
|
||||
let name = source.strip_prefix("/dev/")?;
|
||||
// /dev/mapper/x and /dev/dm-0 (LUKS, LVM) sit on the disks in their slaves/.
|
||||
let name = std::fs::canonicalize(format!("/dev/{name}"))
|
||||
.ok()
|
||||
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
|
||||
.unwrap_or_else(|| name.to_owned());
|
||||
linux_disk_kind(&name, 0)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let df = command_output("df", &[&path.to_string_lossy()])?;
|
||||
let device = df.lines().nth(1)?.split_whitespace().next()?.to_owned();
|
||||
let info = command_output("diskutil", &["info", &device])?;
|
||||
disk_kind_from_diskutil(&info)
|
||||
} else if cfg!(windows) {
|
||||
let letter = path.to_string_lossy().chars().next()?.to_ascii_uppercase();
|
||||
if !letter.is_ascii_alphabetic() {
|
||||
return None;
|
||||
}
|
||||
let out = powershell(&format!(
|
||||
"$d = (Get-Partition -DriveLetter {letter}).DiskNumber; \
|
||||
$p = Get-PhysicalDisk | Where-Object DeviceId -eq $d; \"$($p.BusType) $($p.MediaType)\""
|
||||
))?;
|
||||
let out = out.to_ascii_lowercase();
|
||||
if out.contains("nvme") {
|
||||
Some("nvme")
|
||||
} else if out.contains("ssd") {
|
||||
Some("ssd")
|
||||
} else if out.contains("hdd") {
|
||||
Some("hdd")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The device mounted at the longest mount point above `path`, from `/proc/self/mountinfo`.
|
||||
fn mount_source(mountinfo: &str, path: &Path) -> Option<String> {
|
||||
let path = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_owned());
|
||||
mountinfo
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let fields: Vec<&str> = line.split_whitespace().collect();
|
||||
let point = fields.get(4)?;
|
||||
let dash = fields.iter().position(|f| *f == "-")?;
|
||||
let source = fields.get(dash + 2)?;
|
||||
path.starts_with(point)
|
||||
.then(|| (point.len(), source.to_string()))
|
||||
})
|
||||
.max_by_key(|(len, _)| *len)
|
||||
.map(|(_, source)| source)
|
||||
}
|
||||
|
||||
fn linux_disk_kind(name: &str, depth: u8) -> Option<&'static str> {
|
||||
if depth > 4 {
|
||||
return None;
|
||||
}
|
||||
let class = Path::new("/sys/class/block").join(name);
|
||||
// A mapper device: the first disk under it.
|
||||
if let Ok(slaves) = std::fs::read_dir(class.join("slaves"))
|
||||
&& let Some(slave) = slaves.flatten().next()
|
||||
{
|
||||
return linux_disk_kind(&slave.file_name().to_string_lossy(), depth + 1);
|
||||
}
|
||||
// A partition: its disk is the parent directory.
|
||||
let device = std::fs::canonicalize(&class).ok()?;
|
||||
let disk = if device.join("partition").exists() {
|
||||
device.parent()?.to_owned()
|
||||
} else {
|
||||
device
|
||||
};
|
||||
let disk_name = disk.file_name()?.to_string_lossy();
|
||||
if disk_name.starts_with("nvme") {
|
||||
return Some("nvme");
|
||||
}
|
||||
match std::fs::read_to_string(disk.join("queue/rotational"))
|
||||
.ok()?
|
||||
.trim()
|
||||
{
|
||||
"0" => Some("ssd"),
|
||||
"1" => Some("hdd"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn disk_kind_from_diskutil(info: &str) -> Option<&'static str> {
|
||||
let solid = field_spaced(info, "Solid State")?;
|
||||
if solid != "Yes" {
|
||||
return Some("hdd");
|
||||
}
|
||||
let protocol = field_spaced(info, "Protocol").unwrap_or_default();
|
||||
Some(
|
||||
if ["PCI-Express", "Apple Fabric", "NVMe"]
|
||||
.iter()
|
||||
.any(|p| protocol.contains(p))
|
||||
{
|
||||
"nvme"
|
||||
} else {
|
||||
"ssd"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// ` Key: value` listings (diskutil, system_profiler).
|
||||
fn field_spaced(text: &str, key: &str) -> Option<String> {
|
||||
text.lines().find_map(|l| {
|
||||
let (k, v) = l.split_once(':')?;
|
||||
(k.trim() == key).then(|| v.trim().to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
/// `ac` or `battery`; `None` where it can't be read.
|
||||
pub fn power() -> Option<&'static str> {
|
||||
if cfg!(target_os = "macos") {
|
||||
let batt = command_output("pmset", &["-g", "batt"])?;
|
||||
return Some(if batt.contains("'Battery Power'") {
|
||||
"battery"
|
||||
} else {
|
||||
"ac"
|
||||
});
|
||||
}
|
||||
if cfg!(windows) {
|
||||
// BatteryStatus 1: discharging.
|
||||
let status =
|
||||
powershell("(Get-CimInstance Win32_Battery | Select-Object -First 1).BatteryStatus")?;
|
||||
return Some(if status.trim() == "1" {
|
||||
"battery"
|
||||
} else {
|
||||
"ac"
|
||||
});
|
||||
}
|
||||
let mut on_battery = None;
|
||||
for entry in std::fs::read_dir("/sys/class/power_supply").ok()?.flatten() {
|
||||
let read =
|
||||
|name: &str| std::fs::read_to_string(entry.path().join(name)).unwrap_or_default();
|
||||
match read("type").trim() {
|
||||
"Mains" if read("online").trim() == "1" => return Some("ac"),
|
||||
"Battery" => on_battery = Some("battery"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
on_battery
|
||||
}
|
||||
|
||||
/// `laptop` or `desktop`: the firmware's chassis type, else whether there is a battery.
|
||||
fn form() -> Option<&'static str> {
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(kind) =
|
||||
read("/sys/class/dmi/id/chassis_type").and_then(|t| t.trim().parse::<u8>().ok())
|
||||
{
|
||||
match kind {
|
||||
8 | 9 | 10 | 14 | 30 | 31 | 32 => return Some("laptop"),
|
||||
3..=7 | 13 | 15 | 16 | 35 | 36 => return Some("desktop"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let battery = std::fs::read_dir("/sys/class/power_supply")
|
||||
.ok()?
|
||||
.flatten()
|
||||
.any(|e| {
|
||||
std::fs::read_to_string(e.path().join("type")).is_ok_and(|t| t.trim() == "Battery")
|
||||
});
|
||||
Some(if battery { "laptop" } else { "desktop" })
|
||||
} else if cfg!(target_os = "macos") {
|
||||
let batt = command_output("pmset", &["-g", "batt"])?;
|
||||
Some(if batt.contains("InternalBattery") {
|
||||
"laptop"
|
||||
} else {
|
||||
"desktop"
|
||||
})
|
||||
} else if cfg!(windows) {
|
||||
let n = powershell("@(Get-CimInstance Win32_Battery).Count")?;
|
||||
Some(if n.trim() == "0" { "desktop" } else { "laptop" })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The dedicated memory of the GPU from `vendor` (`nvidia`, `amd`, `intel`, `apple`), in MiB
|
||||
/// rounded to 256, where the driver says; read once. `None` for a GPU sharing the system's.
|
||||
pub fn vram_mb(vendor: &str) -> Option<u32> {
|
||||
static CACHE: OnceLock<std::sync::Mutex<std::collections::HashMap<String, Option<u32>>>> =
|
||||
OnceLock::new();
|
||||
let cache = CACHE.get_or_init(Default::default);
|
||||
let mut cache = cache.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*cache.entry(vendor.to_ascii_lowercase()).or_insert_with(|| {
|
||||
read_vram_mb(&vendor.to_ascii_lowercase()).map(|mb| mb.div_ceil(256) * 256)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_vram_mb(vendor: &str) -> Option<u32> {
|
||||
if cfg!(target_os = "linux") {
|
||||
if vendor.contains("nvidia") {
|
||||
return command_output(
|
||||
"nvidia-smi",
|
||||
&["--query-gpu=memory.total", "--format=csv,noheader,nounits"],
|
||||
)?
|
||||
.lines()
|
||||
.filter_map(|l| l.trim().parse::<u32>().ok())
|
||||
.max();
|
||||
}
|
||||
// amdgpu (and Intel's discrete xe/i915 cards) in sysfs.
|
||||
std::fs::read_dir("/sys/class/drm")
|
||||
.ok()?
|
||||
.flatten()
|
||||
.filter_map(|e| {
|
||||
let dev = e.path().join("device");
|
||||
let bytes = read(&dev.join("mem_info_vram_total").to_string_lossy())
|
||||
.or_else(|| read(&dev.join("lmem_total_bytes").to_string_lossy()))?;
|
||||
bytes.trim().parse::<u64>().ok().map(|b| (b >> 20) as u32)
|
||||
})
|
||||
.filter(|mb| *mb >= 512)
|
||||
.max()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
// Apple silicon shares the system's memory: no "VRAM" line.
|
||||
let text = command_output("system_profiler", &["SPDisplaysDataType"])?;
|
||||
text.lines()
|
||||
.filter_map(|l| {
|
||||
let (k, v) = l.split_once(':')?;
|
||||
k.trim().starts_with("VRAM").then(|| size_mb(v))?
|
||||
})
|
||||
.max()
|
||||
} else if cfg!(windows) {
|
||||
// AdapterRAM stops at 4 GB; the driver's registry value doesn't.
|
||||
let out = powershell(
|
||||
"Get-ItemProperty 'HKLM:\\SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' \
|
||||
-ErrorAction SilentlyContinue | ForEach-Object { \"$($_.DriverDesc)|$($_.'HardwareInformation.qwMemorySize')\" }",
|
||||
)?;
|
||||
out.lines()
|
||||
.filter_map(|l| {
|
||||
let (name, bytes) = l.split_once('|')?;
|
||||
let ok = vendor.is_empty()
|
||||
|| name.to_ascii_lowercase().contains(vendor)
|
||||
|| (vendor == "nvidia" && name.contains("GeForce"))
|
||||
|| (vendor == "amd" && name.contains("Radeon"));
|
||||
ok.then(|| bytes.trim().parse::<u64>().ok().map(|b| (b >> 20) as u32))?
|
||||
})
|
||||
.max()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// "8 GB", "1536 MB" -> MiB.
|
||||
fn size_mb(text: &str) -> Option<u32> {
|
||||
let mut parts = text.split_whitespace();
|
||||
let n: f64 = parts.next()?.parse().ok()?;
|
||||
match parts.next()? {
|
||||
"GB" => Some((n * 1024.0) as u32),
|
||||
"MB" => Some(n as u32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// What doesn't change while the game runs, read once.
|
||||
struct Fixed {
|
||||
cores: u16,
|
||||
base: u32,
|
||||
boost: u32,
|
||||
form: &'static str,
|
||||
}
|
||||
|
||||
fn fixed() -> &'static Fixed {
|
||||
static FIXED: OnceLock<Fixed> = OnceLock::new();
|
||||
FIXED.get_or_init(|| {
|
||||
let (base, boost) = cpu_mhz();
|
||||
Fixed {
|
||||
cores: cores().unwrap_or(0),
|
||||
base: base.unwrap_or(0),
|
||||
boost: boost.unwrap_or(0),
|
||||
form: form().unwrap_or(""),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl System {
|
||||
/// This machine, without the GPU.
|
||||
pub fn detect() -> Self {
|
||||
/// This machine, without the GPU and the screen. `home` is the game folder, for the kind of
|
||||
/// drive it is on.
|
||||
pub fn detect_in(home: Option<&Path>) -> Self {
|
||||
const GIB: f64 = (1u64 << 30) as f64;
|
||||
static STORAGE: OnceLock<&'static str> = OnceLock::new();
|
||||
let fixed = fixed();
|
||||
Self {
|
||||
os: std::env::consts::OS.to_owned(),
|
||||
os_version: os_version().unwrap_or_default(),
|
||||
arch: std::env::consts::ARCH.to_owned(),
|
||||
cpu: cpu().unwrap_or_default(),
|
||||
threads: std::thread::available_parallelism().map_or(0, |n| n.get() as u16),
|
||||
cores: fixed.cores,
|
||||
cpu_base_mhz: fixed.base,
|
||||
cpu_boost_mhz: fixed.boost,
|
||||
ram_gb: ram_bytes().map_or(0, |b| (b as f64 / GIB).round() as u16),
|
||||
storage: home
|
||||
.map(|h| *STORAGE.get_or_init(|| storage(h).unwrap_or("")))
|
||||
.unwrap_or("")
|
||||
.to_owned(),
|
||||
power: power().unwrap_or("").to_owned(),
|
||||
form: fixed.form.to_owned(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// [`System::detect_in`] without the drive.
|
||||
pub fn detect() -> Self {
|
||||
Self::detect_in(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// The GPU's maker from its name and the vendor ID wgpu reports (0: none, as on Metal).
|
||||
pub fn gpu_vendor(name: &str, vendor_id: u32) -> &'static str {
|
||||
match vendor_id {
|
||||
0x10de => return "nvidia",
|
||||
0x1002 | 0x1022 => return "amd",
|
||||
0x8086 => return "intel",
|
||||
0x106b => return "apple",
|
||||
0x5143 => return "qualcomm",
|
||||
_ => {}
|
||||
}
|
||||
let name = name.to_ascii_lowercase();
|
||||
[
|
||||
("apple", "apple"),
|
||||
("nvidia", "nvidia"),
|
||||
("geforce", "nvidia"),
|
||||
("radeon", "amd"),
|
||||
("amd", "amd"),
|
||||
("intel", "intel"),
|
||||
("adreno", "qualcomm"),
|
||||
("llvmpipe", "software"),
|
||||
("lavapipe", "software"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|(needle, _)| name.contains(needle))
|
||||
.map_or("", |(_, v)| v)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -114,11 +523,80 @@ mod tests {
|
||||
assert_eq!(field("NAME=x", "PRETTY_NAME"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cores_are_counted_once_per_package() {
|
||||
let two_cores_four_threads = "processor : 0\nphysical id : 0\ncore id : 0\n\nprocessor : 1\nphysical id : 0\ncore id : 1\n\nprocessor : 2\nphysical id : 0\ncore id : 0\n\nprocessor : 3\nphysical id : 0\ncore id : 1\n";
|
||||
assert_eq!(cores_from_cpuinfo(two_cores_four_threads), Some(2));
|
||||
assert_eq!(
|
||||
cores_from_cpuinfo("processor : 0\nprocessor : 1\n"),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(cores_from_cpuinfo(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_game_folders_mount_is_found() {
|
||||
let mountinfo = "22 1 259:2 / / rw - ext4 /dev/nvme0n1p2 rw\n30 22 8:1 / /mnt/games rw - ext4 /dev/sda1 rw\n";
|
||||
assert_eq!(
|
||||
mount_source(mountinfo, Path::new("/home/x/.horizon")).as_deref(),
|
||||
Some("/dev/nvme0n1p2")
|
||||
);
|
||||
assert_eq!(
|
||||
mount_source(mountinfo, Path::new("/mnt/games/horizon")).as_deref(),
|
||||
Some("/dev/sda1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diskutil_and_sizes_are_read() {
|
||||
let info = " Device Node: /dev/disk3s1\n Protocol: Apple Fabric\n Solid State: Yes\n";
|
||||
assert_eq!(disk_kind_from_diskutil(info), Some("nvme"));
|
||||
assert_eq!(
|
||||
disk_kind_from_diskutil(" Solid State: No\n"),
|
||||
Some("hdd")
|
||||
);
|
||||
assert_eq!(
|
||||
disk_kind_from_diskutil(" Protocol: SATA\n Solid State: Yes\n"),
|
||||
Some("ssd")
|
||||
);
|
||||
assert_eq!(size_mb(" 8 GB"), Some(8192));
|
||||
assert_eq!(size_mb(" 1536 MB"), Some(1536));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gpu_makers() {
|
||||
assert_eq!(gpu_vendor("NVIDIA GeForce RTX 4080", 0x10de), "nvidia");
|
||||
assert_eq!(gpu_vendor("Apple M4 Pro", 0), "apple");
|
||||
assert_eq!(gpu_vendor("AMD Radeon RX 7800 XT", 0), "amd");
|
||||
assert_eq!(
|
||||
gpu_vendor("Mesa Intel(R) HD Graphics 620 (KBL GT2)", 0x8086),
|
||||
"intel"
|
||||
);
|
||||
assert_eq!(gpu_vendor("llvmpipe (LLVM 19)", 0x10005), "software");
|
||||
}
|
||||
|
||||
/// `cargo test -p horizon-telemetry -- --ignored --nocapture what_this_machine_sends`
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn what_this_machine_sends() {
|
||||
let home = std::env::current_dir().unwrap();
|
||||
let mut system = System::detect_in(Some(&home));
|
||||
system.vram_mb = vram_mb("nvidia").or_else(|| vram_mb("amd")).unwrap_or(0);
|
||||
println!("{}", serde_json::to_string_pretty(&system).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_machine_is_described() {
|
||||
let system = System::detect();
|
||||
assert_eq!(system.os, std::env::consts::OS);
|
||||
assert!(system.threads > 0);
|
||||
assert!(system.gpu.is_empty(), "the game fills the GPU in");
|
||||
assert!(system.cores == 0 || system.cores <= system.threads);
|
||||
assert!(["", "ac", "battery"].contains(&system.power.as_str()));
|
||||
assert!(["", "laptop", "desktop"].contains(&system.form.as_str()));
|
||||
let dir = std::env::temp_dir();
|
||||
assert!(
|
||||
["", "nvme", "ssd", "hdd"].contains(&System::detect_in(Some(&dir)).storage.as_str())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ kilobytes whatever the frame rate. Measured numbers are rounded to 3 decimals.
|
||||
"app": { "name": "horizon-client", "version": "0.21.0", "commit": "e96e1571" },
|
||||
"hardware": {
|
||||
"os": "macos", "os_version": "27.0", "arch": "aarch64",
|
||||
"cpu": "Apple M4 Pro", "cpu_threads": 14, "ram_mb": 65536,
|
||||
"cpu": "Apple M4 Pro", "cpu_threads": 14, "cpu_cores": 14, "ram_mb": 65536, "storage": "nvme", "form": "desktop",
|
||||
"gpu": { "name": "Apple M4 Pro", "vendor": "apple", "driver": "", "backend": "metal", "kind": "integrated" },
|
||||
"display": { "width": 2560, "height": 1440, "scale": 2.0, "refresh_hz": 60 }
|
||||
},
|
||||
@@ -58,9 +58,12 @@ one (histogram edges included) makes it 2.
|
||||
optionally `commit` (Horizon builds) and `mods` (`[{ "name", "version" }]`, vanilla with mods).
|
||||
|
||||
**`hardware`.** `os` (`macos`, `linux`, `windows`), `os_version`, `arch`, `cpu` (brand string),
|
||||
`cpu_threads`, `ram_mb`; `gpu` (`name`, `vendor`, `driver`, `backend`: `metal`, `vulkan`, `dx12`, `gl`;
|
||||
`kind`: `integrated`, `discrete`, `virtual`, `cpu`, `other`) and `display` (the screen the window was on:
|
||||
`width`, `height` in physical pixels, `scale`, `refresh_hz`) when known. Any field can be missing.
|
||||
`cpu_threads`, `cpu_cores` (physical), `cpu_base_mhz` and `cpu_boost_mhz` (where the OS gives them; not on
|
||||
Apple silicon), `ram_mb`, `storage` (the drive under the game folder: `nvme`, `ssd`, `hdd`), `form` (`laptop`,
|
||||
`desktop`); `gpu` (`name`, `vendor`, `driver`, `backend`: `metal`, `vulkan`, `dx12`, `gl`;
|
||||
`kind`: `integrated`, `discrete`, `virtual`, `cpu`, `other`; `vram_mb`, a discrete GPU's own memory) and
|
||||
`display` (the screen the window was on: `width`, `height` in physical pixels, `scale`, `refresh_hz`) when
|
||||
known. Any field can be missing. On mains or battery is `conditions.power`.
|
||||
|
||||
**`conditions`.** What has to match for two reports to be compared, all optional: `scenario`, `seed`,
|
||||
`render_distance`, `simulation_distance`, `window` (`[width, height]` actually rendered), `vsync`,
|
||||
|
||||
+33
-4
@@ -9,7 +9,7 @@ The one exception is the anonymous ["I'm playing" ping](#the-im-playing-ping) be
|
||||
`telemetry/consent.json` in the game folder (`HORIZON_HOME`, else `horizon` in the platform data folder):
|
||||
|
||||
```json
|
||||
{ "version": 1, "answered": 1792060800, "choices": { "performance": true, "crashes": true, "bugs": false, "benchmarks": false }, "install_id": "3f2c…" }
|
||||
{ "version": 2, "answered": 1792060800, "choices": { "performance": true, "crashes": true, "bugs": false, "benchmarks": false }, "install_id": "3f2c…" }
|
||||
```
|
||||
|
||||
- `version` is the consent version the player answered (`CONSENT_VERSION`). It goes up when what is collected changes. The launcher then asks again, and until the player answers nothing new is kept or sent.
|
||||
@@ -34,7 +34,24 @@ Every item has `day` (days since 1970, UTC: no finer time), `source` (`launcher`
|
||||
| `bug` | bug reports | `system`, the player's `text` (up to 4000 characters), and the `log` only if they add it after reading it | 80 KiB | 5 |
|
||||
| `benchmark` | benchmark uploads | the benchmark report as `docs/benchmark-report.md` defines it, as it is, with its own schema field | 128 KiB | 10 |
|
||||
|
||||
`system` is the OS and its version, the architecture, the CPU model and thread count, installed memory rounded to the GiB, and the GPU, driver and graphics backend.
|
||||
`system` describes the machine, with what can explain its performance. A field the OS doesn't give is empty or 0:
|
||||
|
||||
| Field | What | Example |
|
||||
|---|---|---|
|
||||
| `os`, `os_version`, `arch` | the OS, its version, the CPU architecture | `linux`, `Fedora Linux 43`, `x86_64` |
|
||||
| `cpu`, `threads`, `cores` | the CPU model, its threads and physical cores | `13th Gen Intel(R) Core(TM) i7-13700KF`, 24, 16 |
|
||||
| `cpu_base_mhz`, `cpu_boost_mhz` | base and boost clock, where the OS gives them (not on Apple silicon) | 3400, 5400 |
|
||||
| `ram_gb` | installed memory, rounded to the GiB | 32 |
|
||||
| `gpu`, `gpu_vendor`, `gpu_kind` | the GPU the game draws with, its maker, `integrated` or `discrete` | `NVIDIA GeForce RTX 4080`, `nvidia`, `discrete` |
|
||||
| `vram_mb` | a discrete GPU's own memory, rounded up to 256 MiB | 16384 |
|
||||
| `driver`, `backend` | the driver version and the graphics API (`Vulkan`, `Metal`, `Dx12`) | `580.95.05`, `Vulkan` |
|
||||
| `storage` | the drive under the game folder: `nvme`, `ssd` or `hdd` | `nvme` |
|
||||
| `display` | the screen the window opened on: `width`, `height` (physical pixels), `refresh_hz`, `scale` | 2560, 1440, 165, 1.0 |
|
||||
| `power`, `form` | on mains or battery (`ac`, `battery`), `laptop` or `desktop` | `ac`, `desktop` |
|
||||
|
||||
The launcher's own items (bug reports sent from it) have no GPU or screen: only the game knows them. The memory given to the game is not a setting in Horizon (no Java heap): `peak_ram_mb` is what it used. Benchmark reports carry the same facts in their `hardware` (`docs/benchmark-report.md`).
|
||||
|
||||
No serial number, host name, user name, MAC address or path is read. Settings → Data collection → "View what would be sent" and `horizon-launcher data show` print every field as it would leave this machine.
|
||||
|
||||
A summary (`frames`, `ticks`) is the count, the 50th, 95th and 99th percentile and the maximum in milliseconds (to the tenth), and 14 bucket counts: under 4 ms, 4–6, 6–8, 8–10, 10–12, 12–14, 14–16.7, 16.7–20, 20–25, 25–33.3, 33.3–50, 50–100, 100–250, and 250 ms and more. Raw frame times never leave: `Timings` records them in tenths of a millisecond (20 KB however long the session) and only its summary is kept. A perf item is under 1 KiB.
|
||||
|
||||
@@ -75,8 +92,20 @@ The launcher reads it when it opens and every minute after, and shows "N playing
|
||||
|
||||
- `POST /v1/batch`
|
||||
- `DELETE /v1/installs/<install_id>`: deletes everything sent under that ID.
|
||||
- `GET /v1/aggregate`: the anonymous totals the website's results page reads.
|
||||
- `GET /v1/benchmarks/summary`: players' benchmark uploads grouped by GPU, CPU, render distance and game for playhorizon.dev/benchmarks. A group shows only with at least 3 different installs, and only its medians and counts: no install ID, day, OS version, driver, memory or screen. Uploads are never published one by one.
|
||||
- `GET /v1/aggregate`: the anonymous totals the website's results page reads. Its `breakdowns` split the play sessions of the period by hardware class (below), each class with its installs, sessions, hours and mean p50 and p99 frame times.
|
||||
- `GET /v1/benchmarks/summary`: players' benchmark uploads grouped by GPU, CPU, render distance and game for playhorizon.dev/benchmarks. A group shows only with at least 3 different installs, and only its medians and counts: no install ID, day, OS version, driver, exact memory or screen. Uploads are never published one by one. Each group's `breakdowns` count its installs per hardware class.
|
||||
- `POST /v1/live` and `GET /v1/live`: the "I'm playing" ping and the live count.
|
||||
|
||||
Public output never holds a raw hardware value, only these classes (`src/classes.rs`, the same in `website/scripts/benchmarks.mjs`), and a class is published only with at least 3 different installs:
|
||||
|
||||
| Breakdown | Classes |
|
||||
|---|---|
|
||||
| `ram` | 4 GB or less, 8 GB, 16 GB, 32 GB, 64 GB or more |
|
||||
| `vram` | shared (integrated GPU), 2 GB or less, 4, 6, 8, 12, 16 GB, more than 16 GB |
|
||||
| `storage` | nvme, ssd, hdd |
|
||||
| `os` | linux, macos, windows |
|
||||
| `gpu_kind` | integrated, discrete |
|
||||
|
||||
The driver, OS version, clocks, screen and power are kept raw for the 180 days of retention, to explain a slow group, and never published.
|
||||
|
||||
`crates/horizon-telemetry-server` is the server, at `https://telemetry.playhorizon.dev` on Railway (one SQLite file on a volume, region `europe-west4`). Its [README](../crates/horizon-telemetry-server/README.md) covers storage (about 190 bytes per play session), retention (180 days of raw rows, anonymous daily totals kept), rate limits and hosting.
|
||||
Generated
+1
@@ -2633,6 +2633,7 @@ dependencies = [
|
||||
name = "horizon-bench"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"horizon-telemetry",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -17,7 +17,7 @@ pub const SWITCHES: [(Kind, &str, &str); 4] = [
|
||||
(
|
||||
Kind::Perf,
|
||||
"Performance statistics",
|
||||
"After each session: frame-time percentiles and a 14-bucket histogram, tick times, peak memory and startup times, with your OS, CPU, GPU, driver, memory size and the settings that change performance.",
|
||||
"After each session: frame-time percentiles and a 14-bucket histogram, tick times, peak memory and startup times, with your machine (OS, CPU and its cores and clocks, memory size, GPU, its memory and driver, drive type, screen size and refresh rate, laptop or desktop, on battery or not) and the settings that change performance.",
|
||||
),
|
||||
(
|
||||
Kind::Crash,
|
||||
@@ -96,7 +96,7 @@ pub fn preview() -> (String, String) {
|
||||
);
|
||||
return (what, upload::preview(&home));
|
||||
}
|
||||
let system = System::detect();
|
||||
let system = with_example_gpu(System::detect_in(Some(&home)));
|
||||
let examples = vec![
|
||||
Item::new(
|
||||
Source::Game,
|
||||
@@ -137,11 +137,29 @@ pub fn preview() -> (String, String) {
|
||||
}))
|
||||
.unwrap_or_default();
|
||||
(
|
||||
"Nothing is waiting. One example of each kind, from this machine (a benchmark upload is the report itself, see docs/benchmark-report.md):".into(),
|
||||
"Nothing is waiting. One example of each kind, from this machine; the game fills in the GPU and screen it uses where these say example (a benchmark upload is the report itself, see docs/benchmark-report.md):".into(),
|
||||
json,
|
||||
)
|
||||
}
|
||||
|
||||
/// What only the game knows (the GPU it draws with, the screen its window is on), as an
|
||||
/// example: the launcher draws with neither.
|
||||
pub fn with_example_gpu(mut system: System) -> System {
|
||||
system.gpu = "example: the GPU the game draws with".into();
|
||||
system.gpu_vendor = "nvidia".into();
|
||||
system.gpu_kind = "discrete".into();
|
||||
system.vram_mb = 8192;
|
||||
system.driver = "example: 570.86".into();
|
||||
system.backend = "Vulkan".into();
|
||||
system.display = Some(horizon_telemetry::payload::Display {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
refresh_hz: 60,
|
||||
scale: 1.0,
|
||||
});
|
||||
system
|
||||
}
|
||||
|
||||
/// "Delete my data": the server deletes everything sent under this install's ID, the waiting
|
||||
/// items go, and the install gets a new ID. If the server can't be reached the ID is kept, so
|
||||
/// the deletion can be asked again.
|
||||
@@ -203,7 +221,7 @@ pub fn bug_report(text: &str, with_log: bool) -> Result<Item, String> {
|
||||
Source::Launcher,
|
||||
backend::VERSION,
|
||||
Payload::Bug(Bug {
|
||||
system: System::detect(),
|
||||
system: System::detect_in(Some(&home())),
|
||||
text: scrubber().scrub(text.trim()),
|
||||
log: with_log.then(log_for_report).flatten(),
|
||||
}),
|
||||
@@ -342,6 +360,20 @@ mod tests {
|
||||
assert!(names(&["everything"]).is_err());
|
||||
}
|
||||
|
||||
/// "View what would be sent" and `data show` list every hardware field.
|
||||
#[test]
|
||||
fn the_example_shows_every_hardware_field() {
|
||||
let json = serde_json::to_string(&with_example_gpu(System::detect())).unwrap();
|
||||
let fields = "os os_version arch cpu threads cores cpu_base_mhz cpu_boost_mhz ram_gb gpu \
|
||||
gpu_vendor gpu_kind vram_mb driver backend storage width height refresh_hz scale power form";
|
||||
for field in fields.split_whitespace() {
|
||||
assert!(
|
||||
json.contains(&format!("\"{field}\":")),
|
||||
"{field} missing from {json}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_kind_has_a_switch() {
|
||||
assert!(Kind::ALL.iter().all(|&k| !label(k).is_empty()));
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
// players' `benchmark` items as the server stores them ({ install_id, day, version, data: <report> } per line).
|
||||
// Players' results are grouped by GPU, CPU, render distance and game, and a group is published only with at least
|
||||
// MIN_INSTALLS installs, so nobody can be picked out. install_id is used to count installs and to weigh each install
|
||||
// once, and never leaves this script; neither do the day, OS version, driver, RAM or screen.
|
||||
// once, and never leaves this script; neither do the day, OS version, driver, the exact RAM and VRAM, or the screen.
|
||||
// A group also says how its installs split by hardware class (RAM, VRAM, drive, OS, integrated or discrete GPU),
|
||||
// each class only with at least MIN_INSTALLS installs. The classes are the server's (src/classes.rs).
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -34,6 +36,43 @@ export function round(field, x) {
|
||||
const trim = (s) => (typeof s === "string" ? s.replace(/\(R\)|\(TM\)|\(tm\)/g, "").replace(/\s+/g, " ").trim() : "");
|
||||
const gameOf = (app) => (app?.name === "horizon-client" ? "horizon" : app?.name === "minecraft" && !(app.mods || []).some((m) => m.name !== "fabric-loader") ? "vanilla" : null);
|
||||
|
||||
// --- Hardware classes: coarse, so that a class holds many machines ---------------------------
|
||||
|
||||
export const DIMENSIONS = ["ram", "vram", "storage", "os", "gpu_kind"];
|
||||
export function ramClass(gb) {
|
||||
if (!(gb > 0)) return null;
|
||||
return gb <= 5 ? "4 GB or less" : gb <= 10 ? "8 GB" : gb <= 20 ? "16 GB" : gb <= 40 ? "32 GB" : "64 GB or more";
|
||||
}
|
||||
export function gpuKindClass(s) {
|
||||
return s === "integrated" || s === "IntegratedGpu" ? "integrated" : s === "discrete" || s === "DiscreteGpu" ? "discrete" : null;
|
||||
}
|
||||
// A GPU's own memory in MiB; an integrated GPU shares the system's.
|
||||
export function vramClass(mb, kind) {
|
||||
if (gpuKindClass(kind) === "integrated") return "shared";
|
||||
if (!(mb > 0)) return null;
|
||||
for (const [max, name] of [[2560, "2 GB or less"], [4608, "4 GB"], [6656, "6 GB"], [8704, "8 GB"], [12800, "12 GB"], [16896, "16 GB"]]) if (mb <= max) return name;
|
||||
return "more than 16 GB";
|
||||
}
|
||||
const oneOf = (list) => (s) => (list.includes(s) ? s : null);
|
||||
export const storageClass = oneOf(["nvme", "ssd", "hdd"]);
|
||||
export const osClass = oneOf(["linux", "macos", "windows"]);
|
||||
// A report's classes, in DIMENSIONS order.
|
||||
export function classesOf(report) {
|
||||
const hw = report?.hardware || {};
|
||||
const kind = hw.gpu?.kind;
|
||||
return [ramClass(finite(hw.ram_mb) ? hw.ram_mb / 1024 : 0), vramClass(hw.gpu?.vram_mb, kind), storageClass(hw.storage), osClass(hw.os), gpuKindClass(kind)];
|
||||
}
|
||||
// Per dimension, the classes with at least minInstalls installs, most first; dimensions without one are left out.
|
||||
export function breakdowns(classes, minInstalls) {
|
||||
const out = {};
|
||||
DIMENSIONS.forEach((dim, i) => {
|
||||
const cells = [...classes[i]].map(([c, set]) => ({ class: c, installs: set.size })).filter((c) => c.installs >= minInstalls);
|
||||
cells.sort((a, b) => b.installs - a.installs || (a.class < b.class ? -1 : a.class > b.class ? 1 : 0));
|
||||
if (cells.length) out[dim] = cells;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// Why a report can't be compared with the others, or null when it can: the standard flight over the four scenes,
|
||||
// VSync off, no frame cap, drawn on a screen.
|
||||
export function rejection(report) {
|
||||
@@ -82,12 +121,17 @@ export function aggregatePlayers(items, { minInstalls = MIN_INSTALLS } = {}) {
|
||||
const report = item?.data;
|
||||
if (!item?.install_id || rejection(report)) continue;
|
||||
const key = JSON.stringify([trim(report.hardware.gpu.name), trim(report.hardware.cpu), report.conditions.render_distance, gameOf(report.app)]);
|
||||
if (!groups.has(key)) groups.set(key, { installs: new Map(), reports: 0, versions: new Set() });
|
||||
if (!groups.has(key)) groups.set(key, { installs: new Map(), reports: 0, versions: new Set(), classes: DIMENSIONS.map(() => new Map()) });
|
||||
const g = groups.get(key);
|
||||
g.reports++;
|
||||
if (item.version) g.versions.add(item.version);
|
||||
if (!g.installs.has(item.install_id)) g.installs.set(item.install_id, []);
|
||||
g.installs.get(item.install_id).push(figures(report));
|
||||
classesOf(report).forEach((c, i) => {
|
||||
if (!c) return;
|
||||
if (!g.classes[i].has(c)) g.classes[i].set(c, new Set());
|
||||
g.classes[i].get(c).add(item.install_id);
|
||||
});
|
||||
}
|
||||
const out = [];
|
||||
for (const [key, g] of groups) {
|
||||
@@ -95,7 +139,7 @@ export function aggregatePlayers(items, { minInstalls = MIN_INSTALLS } = {}) {
|
||||
const [gpu, cpu, render_distance, game] = JSON.parse(key);
|
||||
const perInstall = [...g.installs.values()].map((runs) => Object.fromEntries(FIELDS.map((f) => [f, median(runs.map((r) => r[f]))])));
|
||||
const medians = Object.fromEntries(FIELDS.map((f) => [f, round(f, median(perInstall.map((r) => r[f])))]));
|
||||
out.push({ gpu, cpu, render_distance, game, source: "players", installs: g.installs.size, reports: g.reports, versions: [...g.versions].sort(byVersionDesc).slice(0, 3), medians });
|
||||
out.push({ gpu, cpu, render_distance, game, source: "players", installs: g.installs.size, reports: g.reports, versions: [...g.versions].sort(byVersionDesc).slice(0, 3), medians, breakdowns: breakdowns(g.classes, minInstalls) });
|
||||
}
|
||||
// Most installs first, then by GPU, CPU, render distance and game, compared as plain strings (as the server does).
|
||||
const cmp = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { aggregatePlayers, median, ourRows, rejection, round, summary } from "./benchmarks.mjs";
|
||||
import { aggregatePlayers, median, osClass, ourRows, ramClass, rejection, round, storageClass, summary, vramClass, gpuKindClass } from "./benchmarks.mjs";
|
||||
|
||||
const fixture = (name) => new URL(`./fixtures/benchmarks/${name}`, import.meta.url);
|
||||
const items = readFileSync(fixture("items.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
||||
@@ -17,7 +17,7 @@ test("players' items give the expected groups", () => {
|
||||
|
||||
test("nothing that identifies an install is published", () => {
|
||||
const text = JSON.stringify(aggregatePlayers(items, { minInstalls: 1 }));
|
||||
for (const secret of ["a1", "a2", "install_id", "secret-os-version", "secret-driver", "20394", "ram_mb", "2560"]) assert.ok(!text.includes(secret), secret);
|
||||
for (const secret of ["a1", "a2", "install_id", "secret-os-version", "secret-driver", "20394", "ram_mb", "2560", "16376", "32768", "vram_mb"]) assert.ok(!text.includes(secret), secret);
|
||||
});
|
||||
|
||||
test("a group needs three installs, not three runs", () => {
|
||||
@@ -26,6 +26,30 @@ test("a group needs three installs, not three runs", () => {
|
||||
assert.equal(aggregatePlayers(items, { minInstalls: 2 }).length, 2);
|
||||
});
|
||||
|
||||
test("hardware classes are coarse, as the server's", () => {
|
||||
assert.equal(ramClass(15.6), "16 GB");
|
||||
assert.equal(ramClass(31), "32 GB");
|
||||
assert.equal(ramClass(64), "64 GB or more");
|
||||
assert.equal(ramClass(0), null);
|
||||
assert.equal(vramClass(16376, "discrete"), "16 GB");
|
||||
assert.equal(vramClass(8192, "DiscreteGpu"), "8 GB");
|
||||
assert.equal(vramClass(0, "integrated"), "shared");
|
||||
assert.equal(vramClass(0, ""), null);
|
||||
assert.equal(storageClass("nvme"), "nvme");
|
||||
assert.equal(storageClass("tape"), null);
|
||||
assert.equal(osClass("windows"), "windows");
|
||||
assert.equal(gpuKindClass("virtual"), null);
|
||||
});
|
||||
|
||||
test("a breakdown cell needs three installs too", () => {
|
||||
const [group] = aggregatePlayers(items);
|
||||
assert.equal(group.breakdowns.ram, undefined); // 32 GB has two installs, 16 GB one
|
||||
assert.equal(group.breakdowns.storage, undefined);
|
||||
const [all] = aggregatePlayers(items, { minInstalls: 1 });
|
||||
assert.deepEqual(all.breakdowns.ram, [{ class: "32 GB", installs: 2 }, { class: "16 GB", installs: 1 }]);
|
||||
assert.deepEqual(all.breakdowns.storage, [{ class: "nvme", installs: 2 }, { class: "ssd", installs: 1 }]);
|
||||
});
|
||||
|
||||
test("only comparable runs count", () => {
|
||||
const reasons = items.map((i) => rejection(i.data));
|
||||
assert.deepEqual(reasons.filter(Boolean), ["VSync on or unknown", "offscreen", "frame rate capped", "not every scene", "not the standard flight", "not a version 1 benchmark report"]);
|
||||
|
||||
@@ -7,4 +7,7 @@ Shared by the website's aggregator test (`website/scripts/benchmarks.test.mjs`)
|
||||
RTX 4080 + i7-13700KF at render distance 12 has three installs (one with two runs); the same install at 32,
|
||||
and the RTX 3060 group, have fewer than three and are left out. The other runs are rejected: VSync on,
|
||||
offscreen, a frame cap, a missing scene, a one-scene scenario, a report of another version, no install ID.
|
||||
- Breakdowns: the RTX 4080 group's three installs share a VRAM class (16 GB), the OS and a discrete GPU, so
|
||||
those cells are published; a3 has 16 GB of RAM and an SSD where a1 and a2 have 32 GB and NVMe, so neither the
|
||||
RAM nor the drive cells reach three installs and those dimensions are left out.
|
||||
- `expected.json`: `min_installs` and `groups` of the summary (`generated` varies).
|
||||
@@ -10,7 +10,12 @@
|
||||
"installs": 3,
|
||||
"reports": 4,
|
||||
"versions": ["v0.2.1", "v0.2.0"],
|
||||
"medians": { "avg_fps": 515, "low_1pct_fps": 120, "p95_ms": 3.5, "world_ms": 4500, "chunk_load_ms": 1400, "rss_peak_mb": 1450, "rss_steady_mb": 1250 }
|
||||
"medians": { "avg_fps": 515, "low_1pct_fps": 120, "p95_ms": 3.5, "world_ms": 4500, "chunk_load_ms": 1400, "rss_peak_mb": 1450, "rss_steady_mb": 1250 },
|
||||
"breakdowns": {
|
||||
"vram": [{ "class": "16 GB", "installs": 3 }],
|
||||
"os": [{ "class": "linux", "installs": 3 }],
|
||||
"gpu_kind": [{ "class": "discrete", "installs": 3 }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
{"install_id":"a1","day":20394,"version":"v0.2.0","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":5000,"stdev":null,"min":5000,"max":5000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1400},"memory.rss_steady_mb":{"n":1,"mean":1200},"chunks.village.load_ms":{"n":1,"mean":1500},"chunks.forest.load_ms":{"n":1,"mean":1500},"chunks.ocean.load_ms":{"n":1,"mean":1500},"chunks.mountain.load_ms":{"n":1,"mean":1500}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":400,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":410,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":420,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":430,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a1","day":20395,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":4000,"stdev":null,"min":4000,"max":4000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1500},"memory.rss_steady_mb":{"n":1,"mean":1300},"chunks.village.load_ms":{"n":1,"mean":1300},"chunks.forest.load_ms":{"n":1,"mean":1300},"chunks.ocean.load_ms":{"n":1,"mean":1300},"chunks.mountain.load_ms":{"n":1,"mean":1300}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":600,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":610,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":620,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":630,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a2","day":20395,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":6000,"stdev":null,"min":6000,"max":6000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":1700},"chunks.forest.load_ms":{"n":1,"mean":1700},"chunks.ocean.load_ms":{"n":1,"mean":1700},"chunks.mountain.load_ms":{"n":1,"mean":1700}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":300,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":310,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":320,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":330,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a3","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":3000,"stdev":null,"min":3000,"max":3000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1600},"memory.rss_steady_mb":{"n":1,"mean":1400},"chunks.village.load_ms":{"n":1,"mean":1100},"chunks.forest.load_ms":{"n":1,"mean":1100},"chunks.ocean.load_ms":{"n":1,"mean":1100},"chunks.mountain.load_ms":{"n":1,"mean":1100}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":700,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":710,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":720,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":730,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a4","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":true,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a5","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"offscreen":true,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a6","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"fps_cap":60,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a7","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a8","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"village","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a1","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":32,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":9000,"stdev":null,"min":9000,"max":9000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":2600},"memory.rss_steady_mb":{"n":1,"mean":2300},"chunks.village.load_ms":{"n":1,"mean":6000},"chunks.forest.load_ms":{"n":1,"mean":6000},"chunks.ocean.load_ms":{"n":1,"mean":6000},"chunks.mountain.load_ms":{"n":1,"mean":6000}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":120,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":130,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":140,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":150,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"b1","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":7000,"stdev":null,"min":7000,"max":7000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":2500},"chunks.forest.load_ms":{"n":1,"mean":2500},"chunks.ocean.load_ms":{"n":1,"mean":2500},"chunks.mountain.load_ms":{"n":1,"mean":2500}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":250,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":260,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":270,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":280,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"b2","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":7100,"stdev":null,"min":7100,"max":7100,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":2400},"chunks.forest.load_ms":{"n":1,"mean":2400},"chunks.ocean.load_ms":{"n":1,"mean":2400},"chunks.mountain.load_ms":{"n":1,"mean":2400}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":260,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":270,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":280,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":290,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"b3","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":2,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":7100,"stdev":null,"min":7100,"max":7100,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":2400},"chunks.forest.load_ms":{"n":1,"mean":2400},"chunks.ocean.load_ms":{"n":1,"mean":2400},"chunks.mountain.load_ms":{"n":1,"mean":2400}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":260,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":270,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":280,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":290,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"day":20396,"version":"v0.2.1","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete"},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165}},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":1,"stdev":null,"min":1,"max":1,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1},"memory.rss_steady_mb":{"n":1,"mean":1},"chunks.village.load_ms":{"n":1,"mean":1},"chunks.forest.load_ms":{"n":1,"mean":1},"chunks.ocean.load_ms":{"n":1,"mean":1},"chunks.mountain.load_ms":{"n":1,"mean":1}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":1,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":11,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":21,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":31,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a1","day":20394,"version":"v0.2.0","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":5000,"stdev":null,"min":5000,"max":5000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1400},"memory.rss_steady_mb":{"n":1,"mean":1200},"chunks.village.load_ms":{"n":1,"mean":1500},"chunks.forest.load_ms":{"n":1,"mean":1500},"chunks.ocean.load_ms":{"n":1,"mean":1500},"chunks.mountain.load_ms":{"n":1,"mean":1500}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":400,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":410,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":420,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":430,"low_1pct_fps":100,"low_01pct_fps":50,"p50_ms":2,"p95_ms":4,"p99_ms":8,"p999_ms":12,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a1","day":20395,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":4000,"stdev":null,"min":4000,"max":4000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1500},"memory.rss_steady_mb":{"n":1,"mean":1300},"chunks.village.load_ms":{"n":1,"mean":1300},"chunks.forest.load_ms":{"n":1,"mean":1300},"chunks.ocean.load_ms":{"n":1,"mean":1300},"chunks.mountain.load_ms":{"n":1,"mean":1300}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":600,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":610,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":620,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":630,"low_1pct_fps":140,"low_01pct_fps":70,"p50_ms":2,"p95_ms":3,"p99_ms":6,"p999_ms":9,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a2","day":20395,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":6000,"stdev":null,"min":6000,"max":6000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":1700},"chunks.forest.load_ms":{"n":1,"mean":1700},"chunks.ocean.load_ms":{"n":1,"mean":1700},"chunks.mountain.load_ms":{"n":1,"mean":1700}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":300,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":310,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":320,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":330,"low_1pct_fps":80,"low_01pct_fps":40,"p50_ms":2,"p95_ms":5,"p99_ms":10,"p999_ms":15,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a3","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":16384,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"ssd"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":3000,"stdev":null,"min":3000,"max":3000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1600},"memory.rss_steady_mb":{"n":1,"mean":1400},"chunks.village.load_ms":{"n":1,"mean":1100},"chunks.forest.load_ms":{"n":1,"mean":1100},"chunks.ocean.load_ms":{"n":1,"mean":1100},"chunks.mountain.load_ms":{"n":1,"mean":1100}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":700,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":710,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":720,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":730,"low_1pct_fps":160,"low_01pct_fps":80,"p50_ms":2,"p95_ms":2.5,"p99_ms":5,"p999_ms":7.5,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a4","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":true,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a5","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"offscreen":true,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a6","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"fps_cap":60,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a7","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a8","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"village","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":2000,"stdev":null,"min":2000,"max":2000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1700},"memory.rss_steady_mb":{"n":1,"mean":1500},"chunks.village.load_ms":{"n":1,"mean":900},"chunks.forest.load_ms":{"n":1,"mean":900},"chunks.ocean.load_ms":{"n":1,"mean":900},"chunks.mountain.load_ms":{"n":1,"mean":900}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":900,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":910,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":920,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":930,"low_1pct_fps":200,"low_01pct_fps":100,"p50_ms":2,"p95_ms":2,"p99_ms":4,"p999_ms":6,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"a1","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"13th Gen Intel(R) Core(TM) i7-13700KF","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 4080","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":16376},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":32,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":9000,"stdev":null,"min":9000,"max":9000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":2600},"memory.rss_steady_mb":{"n":1,"mean":2300},"chunks.village.load_ms":{"n":1,"mean":6000},"chunks.forest.load_ms":{"n":1,"mean":6000},"chunks.ocean.load_ms":{"n":1,"mean":6000},"chunks.mountain.load_ms":{"n":1,"mean":6000}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":120,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":130,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":140,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":150,"low_1pct_fps":30,"low_01pct_fps":15,"p50_ms":2,"p95_ms":12,"p99_ms":24,"p999_ms":36,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"b1","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":12288},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":7000,"stdev":null,"min":7000,"max":7000,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":2500},"chunks.forest.load_ms":{"n":1,"mean":2500},"chunks.ocean.load_ms":{"n":1,"mean":2500},"chunks.mountain.load_ms":{"n":1,"mean":2500}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":250,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":260,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":270,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":280,"low_1pct_fps":60,"low_01pct_fps":30,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"b2","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":12288},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":7100,"stdev":null,"min":7100,"max":7100,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":2400},"chunks.forest.load_ms":{"n":1,"mean":2400},"chunks.ocean.load_ms":{"n":1,"mean":2400},"chunks.mountain.load_ms":{"n":1,"mean":2400}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":260,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":270,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":280,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":290,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"install_id":"b3","day":20396,"version":"v0.2.1","source":"game","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":2,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":12288},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":7100,"stdev":null,"min":7100,"max":7100,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1300},"memory.rss_steady_mb":{"n":1,"mean":1100},"chunks.village.load_ms":{"n":1,"mean":2400},"chunks.forest.load_ms":{"n":1,"mean":2400},"chunks.ocean.load_ms":{"n":1,"mean":2400},"chunks.mountain.load_ms":{"n":1,"mean":2400}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":260,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":270,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":280,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":290,"low_1pct_fps":70,"low_01pct_fps":35,"p50_ms":2,"p95_ms":6,"p99_ms":12,"p999_ms":18,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
{"day":20396,"version":"v0.2.1","kind":"benchmark","data":{"format":"horizon-benchmark-report","version":1,"kind":"game","created":"2026-11-02","app":{"name":"horizon-client","version":"0.2.0"},"hardware":{"os":"linux","os_version":"secret-os-version","arch":"x86_64","cpu":"AMD Ryzen 5 5600X 6-Core Processor","cpu_threads":24,"ram_mb":32768,"gpu":{"name":"NVIDIA GeForce RTX 3060","vendor":"nvidia","driver":"secret-driver","backend":"vulkan","kind":"discrete","vram_mb":12288},"display":{"width":2560,"height":1440,"scale":1,"refresh_hz":165},"storage":"nvme"},"conditions":{"scenario":"standard","seed":"1234","render_distance":12,"simulation_distance":12,"window":[1280,720],"vsync":false,"repeats":1,"warmup_s":2},"metrics":{"startup.title_ms":{"n":1,"mean":900,"stdev":null,"min":900,"max":900,"ci95":null},"startup.world_ms":{"n":1,"mean":1,"stdev":null,"min":1,"max":1,"ci95":null},"memory.rss_peak_mb":{"n":1,"mean":1},"memory.rss_steady_mb":{"n":1,"mean":1},"chunks.village.load_ms":{"n":1,"mean":1},"chunks.forest.load_ms":{"n":1,"mean":1},"chunks.ocean.load_ms":{"n":1,"mean":1},"chunks.mountain.load_ms":{"n":1,"mean":1}},"frames":[{"phase":"village","repeat":0,"frames":1000,"seconds":12,"avg_fps":1,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"forest","repeat":0,"frames":1000,"seconds":12,"avg_fps":11,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"ocean","repeat":0,"frames":1000,"seconds":12,"avg_fps":21,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},{"phase":"mountain","repeat":0,"frames":1000,"seconds":12,"avg_fps":31,"low_1pct_fps":1,"low_01pct_fps":0.5,"p50_ms":2,"p95_ms":1,"p99_ms":2,"p999_ms":3,"max_ms":50,"histogram":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}}
|
||||
@@ -26,9 +26,19 @@
|
||||
<li><strong>Performance statistics</strong>, after each play session of a minute or more. These are the frame-time and tick-time percentiles and a 14-bucket histogram (never a recording of each frame), the peak memory, how long the game took to start and to load the world, and the settings that change performance (render and simulation distance, frame limit, VSync, fullscreen, graphics mode).</li>
|
||||
<li><strong>Crash reports</strong>, when the game crashes. They hold the error, where it happened in Horizon's code, your graphics adapter and settings. They hold no log, no world, no server and no resource packs.</li>
|
||||
<li><strong>Bug reports</strong> that you write and send yourself. The end of the game's log goes with one only if you add it after reading it, and lines of chat are removed first.</li>
|
||||
<li><strong>Benchmark uploads</strong> of the benchmarks you run. They are shown publicly on the <a href="/benchmarks">Benchmarks page</a>, only as medians of groups of at least 3 different installs with the same graphics card, processor and render distance; never one by one, and never with your install ID, the day, your OS version, driver, memory or screen.</li>
|
||||
<li><strong>Benchmark uploads</strong> of the benchmarks you run. They are shown publicly on the <a href="/benchmarks">Benchmarks page</a>, only as medians of groups of at least 3 different installs with the same graphics card, processor and render distance; never one by one, and never with your install ID, the day, your OS version, driver, exact memory or screen.</li>
|
||||
</ul>
|
||||
<p>Each one also carries the Horizon version, the day it was made (not the time), and a description of your machine: the operating system and version, the CPU model and thread count, the memory rounded to the gigabyte, and the GPU, its driver and the graphics backend. It also carries a <strong>random install ID</strong>. The launcher makes that ID the first time you allow something; it says nothing about you and exists only so that your data can be deleted.</p>
|
||||
<p>Each one also carries the Horizon version, the day it was made (not the time), and a description of your machine, because that is what explains how fast the game runs on it:</p>
|
||||
<ul>
|
||||
<li>the operating system, its version and the processor architecture;</li>
|
||||
<li>the processor model, its cores and threads, and its base and boost clock where the system gives them;</li>
|
||||
<li>the memory, rounded to the gigabyte;</li>
|
||||
<li>the graphics card the game draws with: its model, maker, whether it is built into the processor or separate, its own memory, its driver version and the graphics interface (Vulkan, Metal, DirectX 12);</li>
|
||||
<li>the kind of drive the game folder is on (NVMe, SSD or hard disk);</li>
|
||||
<li>the screen the game opened on: its size in pixels, refresh rate and scaling;</li>
|
||||
<li>whether the computer is a laptop or a desktop, and whether it runs on battery.</li>
|
||||
</ul>
|
||||
<p>No serial number, computer name, user name, network address or file path is read. Each item also carries a <strong>random install ID</strong>. The launcher makes that ID the first time you allow something; it says nothing about you and exists only so that your data can be deleted.</p>
|
||||
<p>Never sent: your Microsoft or Minecraft account name or UUID, world names, server or LAN addresses, chat, and your user name or home folder in file paths. Before anything is kept, those are replaced with placeholders, along with e-mail and IP addresses. The server does not store the IP address requests come from.</p>
|
||||
<p>The launcher shows exactly what would be sent: Settings → Data collection → View what would be sent, or <code>horizon-launcher data show</code>. Every field is listed in <a href="https://github.com/ludovic111/horizon/blob/main/docs/telemetry.md">docs/telemetry.md</a>.</p>
|
||||
|
||||
@@ -44,7 +54,7 @@
|
||||
<p>For the switches: your consent (Article 6(1)(a) of the EU's General Data Protection Regulation). It is given kind by kind, recorded on your computer with the version of the question you answered and when, and withdrawn at any time with the same switches. Withdrawing deletes what was waiting to be sent. When what is collected changes, the launcher asks again, and nothing new is sent until you answer. The ping holds nothing that identifies you; the address it comes from is used only to limit how often one address may send, kept in memory for an hour at most and never stored.</p>
|
||||
|
||||
<h2>Where and how long</h2>
|
||||
<p>Items wait on your computer (<code>telemetry/</code> in the game folder) and are sent compressed when the launcher starts or closes, to Horizon's own server, <code>telemetry.playhorizon.dev</code>, hosted by <a href="https://railway.com">Railway</a> in the European Union (Amsterdam, the Netherlands). The server keeps them for <strong>180 days</strong>, then deletes them. What stays after that are anonymous totals per day, Horizon version, operating system, GPU maker and render distance (for example "120 sessions on macOS with Apple GPUs at render distance 12"). They hold no install ID, and groups too small to be anonymous are never shown.</p>
|
||||
<p>Items wait on your computer (<code>telemetry/</code> in the game folder) and are sent compressed when the launcher starts or closes, to Horizon's own server, <code>telemetry.playhorizon.dev</code>, hosted by <a href="https://railway.com">Railway</a> in the European Union (Amsterdam, the Netherlands). The server keeps them for <strong>180 days</strong>, then deletes them. What stays after that are anonymous totals per day, Horizon version, operating system, GPU maker and render distance (for example "120 sessions on macOS with Apple GPUs at render distance 12"). They hold no install ID, and groups too small to be anonymous are never shown. Published results also show how players split by memory size (in steps: 8, 16, 32 GB…), graphics memory, drive type, operating system and built-in or separate graphics, each class only once it holds at least 3 different installs; the exact values, the driver, the clocks and the screen are never published.</p>
|
||||
|
||||
<h2>Deleting your data</h2>
|
||||
<p>Settings → Data collection → Delete my data, or <code>horizon-launcher data delete</code>. The server deletes everything sent under your install ID, your share of the totals is recomputed, anything waiting on your computer is removed, and you get a new install ID. You can also turn everything off, or delete the <code>telemetry/</code> folder.</p>
|
||||
|
||||
Reference in new issue
Block a user