Compare commits

...
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 8e31a50c2d Entity tracking: a mob's movement speed looked up once per tick
mob_attributes asked the attribute catalog for movement speed (by name)
and recomputed its base value up to three times per mob and tick, on
top of the registry id built with format! per attribute (Attr::id, the
previous commit). The speed instance, the baby and the sprinting
modifiers now share one lookup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 04:13:27 +01:00
Ludovic MarieandClaude Opus 5.5 82114323d2 Attributes: static registry ids, not format! per mob and tick
The type's default of an attribute (base_value, read by mob movement and
every tick's entity tracking) and the LAN attribute sync built the id
"minecraft:<name>" with format! on each call: for a server's 2000 mobs that
is several allocations per mob per tick. Attr::id returns the id as a
static string.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 04:13:27 +01:00
Ludovic MarieandClaude Opus 5.5 239961568e bench: spawn_natural sub-spans
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 04:13:11 +01:00
Ludovic MarieandClaude Opus 5.5 83092ec8a9 bench: tick_lan sub-spans
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 04:13:11 +01:00
Ludovic MarieandClaude Opus 5.5 6e855070d3 bench: finer streaming spans
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 04:13:11 +01:00
Ludovic MarieandClaude Opus 5.5 a36062d62a bench: streaming sub-spans
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 04:13:11 +01:00
6 changed files with 112 additions and 22 deletions

No files matched your search

+21
View File
@@ -161,3 +161,24 @@ mod tests {
assert!(worked.as_millis() >= 10, "{worked:?} while busy");
}
}
/// Bench only: a span from here to the guard's drop.
pub struct Guard(&'static str, Instant, Option<Duration>);
pub fn guard(name: &'static str) -> Option<Guard> {
enabled().then(|| Guard(name, Instant::now(), thread_cpu()))
}
impl Drop for Guard {
fn drop(&mut self) {
let ms = self.1.elapsed().as_secs_f32() * 1000.0;
let cpu_ms = thread_cpu()
.zip(self.2)
.map_or(ms, |(e, s)| (e - s).as_secs_f32() * 1000.0);
SPANS.with(|s| {
s.borrow_mut().push(Span {
name: self.0,
ms,
cpu_ms,
})
});
}
}
@@ -1240,8 +1240,11 @@ impl Streaming {
let sweep = area != self.scan_area || self.scan_age >= 8;
self.scan_area = area;
self.scan_age = if sweep { 0 } else { self.scan_age + 1 };
let g = crate::spans::guard("st_relight_edits");
self.requeue();
self.relight_edits(world);
drop(g);
let g = crate::spans::guard("st_receive");
let deferred = std::mem::take(&mut self.deferred);
let received = self.shared.done.take();
// Meshes made stale meanwhile, freed with the uploaded ones (`free`).
@@ -1322,6 +1325,8 @@ impl Streaming {
}
}
}
drop(g);
let g = crate::spans::guard("st_sweep");
self.free(stale);
let mut removed = Vec::new();
if sweep {
@@ -1391,10 +1396,16 @@ impl Streaming {
}
self.save_checked_out(written);
}
drop(g);
let g = crate::spans::guard("st_finalize");
drop(g);
let g = crate::spans::guard("st_retain");
let (protos, finishing) = (&self.protos, &self.finishing);
self.decorated.retain(|k| {
protos.contains_key(k) || finishing.contains(k) || world.chunks.contains_key(k)
});
drop(g);
let g = crate::spans::guard("st_finalize");
// Finalize chunks whose neighbourhood is decorated.
let candidates: Vec<(i32, i32)> = self.protos.keys().copied().collect();
for (x, z) in candidates {
@@ -1425,6 +1436,8 @@ impl Streaming {
// Decorate chunks whose 3x3 proto neighbourhood is available, in
// vanilla's order: decoration is order dependent where write regions
// overlap (see `DecorationOrder`).
drop(g);
let g = crate::spans::guard("st_decorate");
let workers = self.threads.len();
if self.decorates && self.order_area != Some((self.center, self.radius)) {
let (decorated, chunks) = (&self.decorated, &world.chunks);
@@ -1546,6 +1559,8 @@ impl Streaming {
self.submit(Some((x, z)), 1, Job::Decorate(x, z, region, finals));
}
}
drop(g);
let g = crate::spans::guard("st_schedule");
self.schedule_generation(world, workers);
// Lighting once all neighbours exist, nearest first and only as
// much of it as the workers can chew. Vanilla's light engine also
@@ -1555,6 +1570,8 @@ impl Streaming {
// grew with the render distance and outlived the columns they
// were queued for (#3629). What stays in `dirty_light` is asked
// for again, from the current centre, as capacity frees.
drop(g);
let g = crate::spans::guard("st_light");
let mut light: Vec<(i64, i32, i32)> = Vec::new();
let mut gone: Vec<(i32, i32)> = Vec::new();
for &(x, z) in world.dirty_light.iter() {
@@ -1567,7 +1584,20 @@ impl Streaming {
for key in gone {
world.dirty_light.remove(&key);
}
drop(g);
let g = crate::spans::guard("st_light_submit");
let room = (self.threads.len() * 4).saturating_sub(self.lighting.len());
if crate::spans::enabled() && self.sequence % 50 == 0 {
log::info!(
"stdbg dirty_light {} ready {} room {} decorated {} protos {} chunks {}",
world.dirty_light.len(),
light.len(),
room,
self.decorated.len(),
self.protos.len(),
world.chunks.len()
);
}
nearest_first(&mut light, room);
for (_, x, z) in light {
let Some(region) = self.light_region(world, x, z) else {
@@ -1584,6 +1614,7 @@ impl Streaming {
Job::Light(x, z, region, self.sequence, self.sky_light),
);
}
drop(g);
// A server draws nothing: its sections are never meshed.
if !self.mesh {
world.dirty_sections.clear();
+14
View File
@@ -2194,10 +2194,16 @@ impl Game<'_> {
.iter()
.filter_map(|g| Some((g.vehicle?, g.entity_id)))
.collect();
let g = horizon_client::spans::guard("ln_riding");
self.lan_riding(host_entity, &guests, &mut host.hub.riding);
drop(g);
let g = horizon_client::spans::guard("ln_entities");
self.lan_entities(host_entity, &host.mob_players, &mut host.hub, &mut states);
drop(g);
let g = horizon_client::spans::guard("ln_hooks_uuids");
self.lan_hook_states(host_entity, &host.play, &host.hub, &mut states);
self.lan_entity_uuids(&mut states);
drop(g);
} else {
states.clear();
}
@@ -2267,6 +2273,7 @@ impl Game<'_> {
// Player packets may already have been drained before
// scheduled block ticks (`tick_lan_packets`).
let pass = self.level_pass;
let g = horizon_client::spans::guard("ln_poll");
let mut events = if pass.first && !pass.packets_drained {
host.hub.poll(&mut ctx)
} else {
@@ -2275,7 +2282,10 @@ impl Game<'_> {
if !pass.packets_drained {
events.extend(host.hub.drain_packets(&mut ctx));
}
drop(g);
let g = horizon_client::spans::guard("ln_tick_level");
events.extend(host.hub.tick_level(&mut ctx));
drop(g);
if pass.last {
host.hub.end_tick();
}
@@ -2288,6 +2298,7 @@ impl Game<'_> {
host.hub.set_host_game_mode(state.uuid, state.game_mode);
}
self.wake_woken_guests(&mut host);
let g = horizon_client::spans::guard("ln_events");
let Some(back) = self.lan_process_events(host, events, state.as_ref(), entities, spawn)
else {
return;
@@ -2319,9 +2330,12 @@ impl Game<'_> {
.take()
.expect("the hub is back after the menu events");
}
let g = horizon_client::spans::guard("ln_guests");
if !paused {
self.tick_guests(&mut host);
}
drop(g);
let _g = horizon_client::spans::guard("ln_flush");
self.lan = Some(host);
// A blast from a guest's attack, and a mace's wind burst.
self.flush_lan_explosions();
@@ -116,20 +116,22 @@ impl Game<'_> {
/// `/attribute` changed.
pub(super) fn mob_attributes(&self, m: &Mob, out: &mut Vec<AttributeSnapshot>) {
let kind = m.kind.id.as_str();
if let Some(a) = self.synced_attribute(kind, "minecraft:movement_speed") {
instance(out, a, finalize::base_value(m, Attr::MovementSpeed));
// Movement speed is synced for nearly every mob, and read up to three
// times below: looked up once per mob and tick.
let speed = self
.synced_attribute(kind, Attr::MovementSpeed.id())
.map(|a| (a, finalize::base_value(m, Attr::MovementSpeed)));
if let Some((a, base)) = speed {
instance(out, a, base);
}
if let Some(s) = m.extra.attributes.as_deref() {
for (attr, v) in &s.bases {
if let Some(a) = self.synced_attribute(kind, &format!("minecraft:{}", attr.name()))
{
if let Some(a) = self.synced_attribute(kind, attr.id()) {
instance(out, a, *v).base = *v;
}
}
for x in &s.modifiers {
if let Some(a) =
self.synced_attribute(kind, &format!("minecraft:{}", x.attr.name()))
{
if let Some(a) = self.synced_attribute(kind, x.attr.id()) {
add_modifier(
out,
a,
@@ -162,18 +164,17 @@ impl Game<'_> {
if rolled
&& !jump_base
&& m.extra.vector[0] > 0.0
&& let Some(a) = self.synced_attribute(kind, "minecraft:jump_strength")
&& let Some(a) = self.synced_attribute(kind, Attr::JumpStrength.id())
{
instance(out, a, m.extra.vector[0]).base = m.extra.vector[0];
}
if let (Some(a), Some((amount, Op::AddMultipliedBase))) = (
self.synced_attribute(kind, "minecraft:movement_speed"),
finalize::transient(m, Attr::MovementSpeed),
) {
if let (Some((a, base)), Some((amount, Op::AddMultipliedBase))) =
(speed, finalize::transient(m, Attr::MovementSpeed))
{
add_modifier(
out,
a,
finalize::base_value(m, Attr::MovementSpeed),
base,
AttributeModifier {
id: "minecraft:baby".into(),
amount,
@@ -182,14 +183,11 @@ impl Game<'_> {
);
}
// LivingEntity.setSprinting: SPEED_MODIFIER_SPRINTING (a dashing cat or ocelot).
if let (true, Some(a)) = (
m.extra.sprinting,
self.synced_attribute(kind, "minecraft:movement_speed"),
) {
if let (true, Some((a, base))) = (m.extra.sprinting, speed) {
add_modifier(
out,
a,
finalize::base_value(m, Attr::MovementSpeed),
base,
AttributeModifier {
id: "minecraft:sprinting".into(),
amount: f64::from(0.3f32),
@@ -526,11 +526,22 @@ impl<'a> Game<'a> {
if players.is_empty() {
return;
}
let g = horizon_client::spans::guard("sn_chunks");
let mut scratch = std::mem::take(&mut self.spawn_scratch);
let spawnable = self.spawning_chunks(&players, &mut scratch);
if horizon_client::spans::enabled() && self.world.game_time % 200 == 0 {
log::info!(
"sndbg mobs {} candidates {} players {}",
self.mobs.len(),
scratch.candidates.len(),
players.len()
);
}
for &key in &scratch.candidates {
*self.world.inhabited.entry(key).or_insert(0) += 1;
}
drop(g);
let g = horizon_client::spans::guard("sn_state");
let spawn_mobs = self.ui.rules.bool("spawn_mobs");
let spawn_enemies = self.ui.rules.bool("spawn_monsters");
let mut random = std::mem::replace(&mut self.world.random, LegacyRandom::new(0));
@@ -575,6 +586,8 @@ impl<'a> Game<'a> {
let j = random.next_int_bound(i as i32 + 1).unwrap_or(0) as usize;
scratch.candidates.swap(i, j);
}
drop(g);
let _g = horizon_client::spans::guard("sn_loop");
if !categories.is_empty() {
for chunk in scratch.candidates.drain(..) {
let mut spawn = |l: &GameLevel, r: &SpawnRequest| {
@@ -599,6 +612,7 @@ impl<'a> Game<'a> {
self.world.random = random;
// ServerChunkCache.lastSpawnState, for the debug overlay.
self.spawn_counts.insert(self.dim, last_state);
let _g = horizon_client::spans::guard("sn_add");
self.add_spawned(pending, false);
self.spawn_scratch = scratch;
}
+16 -4
View File
@@ -435,6 +435,21 @@ impl Attr {
Self::BurningTime => "burning_time",
}
}
/// The attribute's registry id (`minecraft:` and its name), without
/// building it: mob ticks and entity tracking ask for every mob.
pub fn id(self) -> &'static str {
match self {
Self::MaxHealth => "minecraft:max_health",
Self::MovementSpeed => "minecraft:movement_speed",
Self::FollowRange => "minecraft:follow_range",
Self::AttackDamage => "minecraft:attack_damage",
Self::KnockbackResistance => "minecraft:knockback_resistance",
Self::SpawnReinforcements => "minecraft:spawn_reinforcements",
Self::JumpStrength => "minecraft:jump_strength",
Self::Armor => "minecraft:armor",
Self::BurningTime => "minecraft:burning_time",
}
}
/// `RangedAttribute` bounds.
fn range(self) -> (f64, f64) {
match self {
@@ -473,10 +488,7 @@ pub struct Attributes {
}
/// The type's default value of an attribute, if the mob has it.
pub fn default_value(m: &Mob, a: Attr) -> Option<f64> {
m.kind
.attributes
.get(&format!("minecraft:{}", a.name()))
.copied()
m.kind.attributes.get(a.id()).copied()
}
pub fn base_value(m: &Mob, a: Attr) -> f64 {
m.extra