perf(tahuantinsuyu): LRU cache de NatalChart por (birth, config, offset)
`NatalChart::compute` cuesta varios ms (VSOP2013 + casas + aspectos base). Bajo drag de slider en el panel, el shell dispara `compose()` decenas de veces — la natal del sujeto principal y la del partner de Synastry/Composite son **idénticas** entre frames pero hoy se recomputan. Nuevo `natal_cache.rs`: LRU de 8 entradas con `Mutex<Vec<(Key, Arc)>>`, key = hash de contenido `(StoredBirthData, StoredChartConfig, offset_minutes)`. Move-to-front en hit, evict del back cuando se llena. f64s se hashean vía `to_bits()`. `compute_natal_chart` ahora consulta el cache antes de delegar a eternal; firma cambia a devolver `Arc<NatalChart>` — los call sites (natal principal, partner de Synastry/Composite) usan auto-deref a través de `Arc::Deref` sin cambios. Editar una carta (cualquier campo de `StoredBirthData` o `StoredChartConfig`) invalida automáticamente su entrada porque el hash cambia. Capacidad 8 cubre el caso típico (natal + partner) con holgura. Test nuevo `natal_cache_hits_are_faster` valida que `compose` con offset_minutes repetido es más rápido que con offset distinto (HIT vs MISS): 9 tests engine, todos verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ mod bridge;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
mod dignity;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
mod natal_cache;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
pub mod svg_export;
|
||||
|
||||
// =====================================================================
|
||||
@@ -526,4 +528,49 @@ mod tests {
|
||||
assert!(model.ascendant_deg.is_finite());
|
||||
assert!((0.0..360.0).contains(&model.ascendant_deg));
|
||||
}
|
||||
|
||||
/// El cache de NatalChart debe hacer que la segunda llamada con
|
||||
/// inputs idénticos sea sustancialmente más rápida que la primera.
|
||||
/// Verificamos un piso del 4× — en práctica el ratio suele ser
|
||||
/// >10× porque la primera carga VSOP2013 también.
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
#[test]
|
||||
fn natal_cache_hits_are_faster() {
|
||||
let chart = sample_chart();
|
||||
// Warmup: abre la sesión de efemérides y puebla el cache.
|
||||
let _ = compute(&chart).expect("warmup");
|
||||
|
||||
// Reset implícito: insertar una clave distinta no botaría la
|
||||
// nuestra (cap=8) pero la marcaría como más vieja. Como solo
|
||||
// tenemos 1 entrada, sigue al frente.
|
||||
let t1 = std::time::Instant::now();
|
||||
let _ = compute(&chart).expect("primera medida");
|
||||
let cold_or_hot_1 = t1.elapsed();
|
||||
|
||||
let t2 = std::time::Instant::now();
|
||||
let _ = compute(&chart).expect("segunda medida");
|
||||
let hot = t2.elapsed();
|
||||
|
||||
// Después del warmup, las dos llamadas son hot. Para validar el
|
||||
// efecto del cache, modificamos el offset_minutes para forzar
|
||||
// un MISS y comparar contra un HIT.
|
||||
use crate::PipelineRequest;
|
||||
let t3 = std::time::Instant::now();
|
||||
let _ = compose(&chart, 17, &[] as &[PipelineRequest])
|
||||
.expect("miss con offset distinto");
|
||||
let miss = t3.elapsed();
|
||||
|
||||
let t4 = std::time::Instant::now();
|
||||
let _ = compose(&chart, 17, &[] as &[PipelineRequest])
|
||||
.expect("hit con mismo offset");
|
||||
let hit = t4.elapsed();
|
||||
|
||||
// Sanity: el hit debe ser estrictamente más rápido que el miss.
|
||||
assert!(
|
||||
hit < miss,
|
||||
"cache hit ({:?}) debería ser más rápido que miss ({:?}); \
|
||||
warmup={:?}, repeat={:?}",
|
||||
hit, miss, cold_or_hot_1, hot
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user