diff --git a/rust/prop_grid_rs/src/decoder.rs b/rust/prop_grid_rs/src/decoder.rs index cc22004e..0ef74453 100644 --- a/rust/prop_grid_rs/src/decoder.rs +++ b/rust/prop_grid_rs/src/decoder.rs @@ -40,7 +40,17 @@ pub struct Message { /// Per-cell: `"VAR:LEVEL" -> f32`. Key format matches Elixir exactly so /// scorer input can be wired unchanged. -pub type CellValues = HashMap; +/// +/// Keys are `Arc` rather than `String`: the hot decode loop +/// inserts the same `"VAR:LEVEL"` string into ~92k cell maps per +/// GRIB2 message, which was previously a String::clone per cell +/// (~5.5M heap allocations + frees per chain step across 60 +/// messages). `Arc::clone` is a single atomic increment — keys +/// allocate once per message and are ref-counted thereafter. +/// +/// Lookup is unchanged: `cell.get("TMP:2 m above ground")` works +/// because `Arc: Borrow`. +pub type CellValues = HashMap, f32>; /// Lat/lon rounded to 3 decimals. f64 keys can't be hashed directly, so /// we key by `(lat_mdeg, lon_mdeg)` where `*_mdeg = round(x * 1000) as i32`. @@ -224,7 +234,10 @@ pub fn parse_lola_binary(bin: &[u8], messages: &[Message], grid_spec: &GridSpec) continue; } let chunk = &bin[data_offset..data_offset + bytes_per_msg]; - let key = format!("{}:{}", msg.var, msg.level); + // Allocate the "VAR:LEVEL" string once per message. Every + // subsequent cell insertion is Arc::clone (one atomic inc), + // not a String::clone (heap alloc + copy + free). + let key: std::sync::Arc = format!("{}:{}", msg.var, msg.level).into(); for j in 0..ny { let lat = round3(grid_spec.lat_start + j as f64 * grid_spec.lat_step); @@ -240,7 +253,7 @@ pub fn parse_lola_binary(bin: &[u8], messages: &[Message], grid_spec: &GridSpec) let lon_key = (lon * 1000.0).round() as i32; out.entry((lat_key, lon_key)) .or_default() - .insert(key.clone(), value); + .insert(std::sync::Arc::clone(&key), value); } } } diff --git a/rust/prop_grid_rs/src/native_duct.rs b/rust/prop_grid_rs/src/native_duct.rs index dd54a0a2..bbfd7539 100644 --- a/rust/prop_grid_rs/src/native_duct.rs +++ b/rust/prop_grid_rs/src/native_duct.rs @@ -127,18 +127,25 @@ pub fn build_native_profile(cell: &CellValues) -> Option { // or TMP is absent — those levels are junk. let mut levels: Vec<(f64, f64, f64, f64)> = Vec::with_capacity(NATIVE_LEVEL_COUNT as usize); for level in 1..=NATIVE_LEVEL_COUNT { + // `cell.get` dispatches via Arc's Borrow, so pass + // &str refs built by `format!(…).as_str()` (or inline &str + // concat) — passing `&String` triggers the wrong Borrow impl. let lvl_str = format!("{level} hybrid level"); - let Some(&hgt) = cell.get(&format!("HGT:{lvl_str}")) else { + let hgt_key = format!("HGT:{lvl_str}"); + let tmp_key = format!("TMP:{lvl_str}"); + let spfh_key = format!("SPFH:{lvl_str}"); + let pres_key = format!("PRES:{lvl_str}"); + let Some(&hgt) = cell.get(hgt_key.as_str()) else { continue; }; - let Some(&tmp) = cell.get(&format!("TMP:{lvl_str}")) else { + let Some(&tmp) = cell.get(tmp_key.as_str()) else { continue; }; // SPFH / PRES default to 0.0 if absent — the duct math handles // degenerate rows (returns 0.0 gradient / no duct) without // raising, matching Elixir's nil-tolerance. - let spfh = cell.get(&format!("SPFH:{lvl_str}")).copied().unwrap_or(0.0); - let pres = cell.get(&format!("PRES:{lvl_str}")).copied().unwrap_or(0.0); + let spfh = cell.get(spfh_key.as_str()).copied().unwrap_or(0.0); + let pres = cell.get(pres_key.as_str()).copied().unwrap_or(0.0); levels.push((hgt as f64, tmp as f64, spfh as f64, pres as f64)); } @@ -221,14 +228,14 @@ mod tests { fn cell_with_levels(count: usize) -> CellValues { let mut c = CellValues::new(); for i in 1..=count { - c.insert(format!("HGT:{i} hybrid level"), (i as f32) * 50.0); - c.insert(format!("TMP:{i} hybrid level"), 290.0 - (i as f32) * 0.5); + c.insert(format!("HGT:{i} hybrid level").into(), (i as f32) * 50.0); + c.insert(format!("TMP:{i} hybrid level").into(), 290.0 - (i as f32) * 0.5); c.insert( - format!("SPFH:{i} hybrid level"), + format!("SPFH:{i} hybrid level").into(), 0.008 - (i as f32) * 0.0001, ); c.insert( - format!("PRES:{i} hybrid level"), + format!("PRES:{i} hybrid level").into(), 101_000.0 - (i as f32) * 600.0, ); }