diff --git a/benchmarks/shared-json-historical-20261017.json b/benchmarks/shared-json-historical-20261017.json new file mode 100644 index 0000000000..2e0e7538f7 --- /dev/null +++ b/benchmarks/shared-json-historical-20261017.json @@ -0,0 +1,188 @@ +{ + "scope": "Assets + block facts + behaviours loader only; excludes full startup", + "resources": ".local/horizon-client/26.3", + "repeats": 5, + "comparison": "historical cf837118 release binary", + "reference_binary": "target/release/examples/shared_json_startup_profile-cf837118", + "metrics": { + "baseline": { + "phase_ms": { + "median": 1462.003, + "samples": [ + 1639.999, + 1506.864, + 1429.844, + 1462.003, + 1346.061 + ] + }, + "total_ms": { + "median": 1922.795, + "samples": [ + 3383.516, + 1990.554, + 1912.354, + 1922.795, + 1777.912 + ] + }, + "cpu_ms": { + "median": 1961.459, + "samples": [ + 2419.086, + 2021.457, + 1951.587, + 1961.459, + 1839.984 + ] + }, + "peak_rss_kib": { + "median": 820912, + "samples": [ + 819956, + 821052, + 820912, + 821792, + 820424 + ] + } + }, + "pr607": { + "phase_ms": { + "median": 486.715, + "samples": [ + 460.738, + 442.013, + 623.053, + 486.715, + 550.652 + ] + }, + "total_ms": { + "median": 948.089, + "samples": [ + 923.057, + 935.599, + 1109.64, + 948.089, + 1052.546 + ] + }, + "cpu_ms": { + "median": 968.424, + "samples": [ + 928.489, + 946.932, + 1128.431, + 968.424, + 1079.642 + ] + }, + "peak_rss_kib": { + "median": 351796, + "samples": [ + 351796, + 351844, + 351536, + 351532, + 351988 + ] + } + }, + "cf837118": { + "phase_ms": { + "median": 474.268, + "samples": [ + 443.122, + 474.268, + 520.497, + 433.525, + 532.058 + ] + }, + "total_ms": { + "median": 945.87, + "samples": [ + 934.722, + 945.87, + 1006.539, + 888.201, + 1032.6 + ] + }, + "cpu_ms": { + "median": 953.668, + "samples": [ + 941.957, + 953.668, + 1029.122, + 903.574, + 1065.025 + ] + }, + "peak_rss_kib": { + "median": 274476, + "samples": [ + 273448, + 274844, + 274476, + 274556, + 274252 + ] + } + }, + "current": { + "phase_ms": { + "median": 445.924, + "samples": [ + 472.165, + 427.797, + 419.288, + 445.924, + 524.211 + ] + }, + "total_ms": { + "median": 999.669, + "samples": [ + 999.669, + 892.616, + 1051.427, + 940.595, + 1112.158 + ] + }, + "cpu_ms": { + "median": 1001.24, + "samples": [ + 1001.24, + 887.874, + 1051.732, + 951.495, + 1132.208 + ] + }, + "peak_rss_kib": { + "median": 267604, + "samples": [ + 267716, + 267920, + 267448, + 267604, + 267344 + ] + } + } + }, + "builds": { + "current": "release example built at cf837118 + current uncommitted delta in PR #607 worktree", + "historical": "release example built at exact cf837118 commit, with same Cargo release profile and shared dependency target" + }, + "mode_commits": { + "baseline": "independent loader algorithm at e422bd13, executed in current binary", + "pr607": "shared early construction algorithm at a01d47f2, executed in current binary", + "cf837118": "exact historical binary at cf837118, deferred mode", + "current": "current PR worktree, deferred mode with interned strings" + }, + "limitations": "Loader-only (Assets + BlockFacts + Behaviours), no full client startup or first-frame timing. Same release profile but different compilation contexts for historical binary; hardware activity and cache warmth vary. CPU and elapsed differences between cf837118 and current are mixed and may be noise. Peak RSS is whole-process Linux ru_maxrss." +} diff --git a/crates/horizon-client/examples/shared_json_startup_profile.rs b/crates/horizon-client/examples/shared_json_startup_profile.rs index d6d4ba2fec..108e8b29d6 100644 --- a/crates/horizon-client/examples/shared_json_startup_profile.rs +++ b/crates/horizon-client/examples/shared_json_startup_profile.rs @@ -9,9 +9,11 @@ use std::{path::Path, time::Instant}; fn main() -> Result<(), Box> { let mut args = std::env::args().skip(1); let root = args.next().ok_or("expected resource directory")?; - let mode = args.next().ok_or("expected baseline, pr607 or shared")?; + let mode = args + .next() + .ok_or("expected baseline, pr607, shared or deferred")?; if !["baseline", "pr607", "shared", "deferred"].contains(&mode.as_str()) { - return Err("expected baseline, pr607 or shared".into()); + return Err("expected baseline, pr607, shared or deferred".into()); } let root = Path::new(&root); let total = Instant::now(); diff --git a/crates/horizon-client/src/blocks.rs b/crates/horizon-client/src/blocks.rs index 7c0b64f8d6..27a36396ad 100644 --- a/crates/horizon-client/src/blocks.rs +++ b/crates/horizon-client/src/blocks.rs @@ -117,16 +117,24 @@ pub struct GameplayProjection { emission: u64, dampening: u64, hardness: f64, - push: Box, + push: Arc, volume: f64, pitch: f64, - place: Box, - break_sound: Box, - fall: Box, + place: Arc, + break_sound: Arc, + fall: Arc, } impl GameplayProjection { - fn from_entry(entry: &decode::Entry<'_>) -> Self { + fn from_entry(entry: &decode::Entry<'_>, strings: &mut HashMap>) -> Self { + fn intern(strings: &mut HashMap>, value: &str) -> Arc { + if let Some(existing) = strings.get(value) { + return Arc::clone(existing); + } + let value_arc: Arc = Arc::from(value); + strings.insert(value.to_owned(), Arc::clone(&value_arc)); + value_arc + } const FLAGS: [&str; 11] = [ "replaceable", "random_ticks", @@ -147,12 +155,12 @@ impl GameplayProjection { emission: entry.unsigned("emission"), dampening: entry.unsigned("dampening"), hardness: entry.number("hardness"), - push: entry.text("push").into(), + push: intern(strings, entry.text("push")), volume: entry.sound_number("volume"), pitch: entry.sound_number("pitch"), - place: entry.sound_id("place").into(), - break_sound: entry.sound_id("break").into(), - fall: entry.sound_id("fall").into(), + place: intern(strings, entry.sound_id("place")), + break_sound: intern(strings, entry.sound_id("break")), + fall: intern(strings, entry.sound_id("fall")), } } } @@ -239,7 +247,11 @@ impl BlockFacts { behaviours: &Value, ) -> Result<(Self, Vec)> { Self::load_with_callback(root, registry, behaviours, |entries| { - Ok(entries.iter().map(GameplayProjection::from_entry).collect()) + let mut strings = HashMap::new(); + Ok(entries + .iter() + .map(|entry| GameplayProjection::from_entry(entry, &mut strings)) + .collect()) }) } diff --git a/crates/horizon-client/src/mod_content_tests.rs b/crates/horizon-client/src/mod_content_tests.rs index 1ed154f33f..66ad8eb2f7 100644 --- a/crates/horizon-client/src/mod_content_tests.rs +++ b/crates/horizon-client/src/mod_content_tests.rs @@ -349,6 +349,35 @@ fn independent_reloads_read_changed_behaviour_json_and_report_invalid_files() { ); assert!(horizon_world::level::Behaviours::load(registry, &scratch).is_ok()); + // Override one exported state bit at the same path and verify that a later + // independent and paired read sees it while the original resource root does not. + let mut changed: serde_json::Value = serde_json::from_slice(&original).unwrap(); + let original_bits = changed["states"][0][1].as_u64().unwrap(); + changed["states"][0][1] = (original_bits ^ 1).into(); + fs::write( + scratch.join("block-behaviours.json"), + serde_json::to_vec(&changed).unwrap(), + ) + .unwrap(); + let changed_pair = BlockFacts::load_pair(&scratch, registry).unwrap(); + let changed_independent = BlockFacts::load(&scratch, registry).unwrap(); + let base_facts = BlockFacts::load(&base, registry).unwrap(); + let state = horizon_world::registry::BlockStateId(0); + assert_ne!( + changed_pair.0.get(state).legacy_solid, + base_facts.get(state).legacy_solid + ); + assert_eq!( + changed_pair.0.get(state).legacy_solid, + changed_independent.get(state).legacy_solid + ); + // The world loader sees the same overridden legacy solidity flag. + let changed_world = horizon_world::level::Behaviours::load(registry, &scratch).unwrap(); + assert_eq!( + changed_world.facts(state).flags & 1 != 0, + changed_pair.0.get(state).legacy_solid + ); + fs::write(scratch.join("block-behaviours.json"), b"{").unwrap(); assert!(BlockFacts::load_pair(&scratch, registry).is_err()); assert!(BlockFacts::load(&scratch, registry).is_err()); diff --git a/tools/profile_client_startup.py b/tools/profile_client_startup.py index 1a9442e625..bd648b7c97 100644 --- a/tools/profile_client_startup.py +++ b/tools/profile_client_startup.py @@ -40,6 +40,8 @@ def main(): parser.add_argument("--binary", type=Path, default=Path("target/release/examples/shared_json_startup_profile")) parser.add_argument("--resources", type=Path, default=Path(".local/horizon-client/26.3")) parser.add_argument("--repeats", type=int, default=5) + parser.add_argument("--reference-binary", type=Path, + help="Release binary built from cf837118; compare its deferred mode against current") parser.add_argument("--output", type=Path) args = parser.parse_args() if args.repeats < 1: @@ -47,14 +49,31 @@ def main(): binary, resources = args.binary.resolve(), args.resources.resolve() if not binary.is_file() or not resources.is_dir(): parser.error("binary or resources missing; build the example and prepare resources first") - samples = {"baseline": [], "pr607": [], "shared": [], "deferred": []} + if args.reference_binary: + reference = args.reference_binary.resolve() + if not reference.is_file(): + parser.error("reference binary missing") + modes = ["baseline", "pr607", "cf837118", "current"] + commands = { + "baseline": (binary, "baseline"), + "pr607": (binary, "pr607"), + "cf837118": (reference, "deferred"), + "current": (binary, "deferred"), + } + else: + modes = ["baseline", "pr607", "shared", "deferred"] + commands = {mode: (binary, mode) for mode in modes} + samples = {mode: [] for mode in modes} for i in range(args.repeats): - for mode in (("baseline", "pr607", "shared", "deferred") if i % 2 == 0 else ("deferred", "shared", "pr607", "baseline")): - samples[mode].append(run(binary, resources, mode)) + for mode in (modes if i % 2 == 0 else list(reversed(modes))): + executable, binary_mode = commands[mode] + samples[mode].append(run(executable, resources, binary_mode)) result = { "scope": "Assets + block facts + behaviours loader only; excludes full startup", "resources": str(resources), "repeats": args.repeats, + "comparison": "historical cf837118 release binary" if args.reference_binary else "same binary loading modes", + "reference_binary": str(args.reference_binary) if args.reference_binary else None, "metrics": {mode: {key: {"median": round(statistics.median(row[key] for row in rows), 3), "samples": [round(row[key], 3) for row in rows]} for key in rows[0]} for mode, rows in samples.items()},