perf(presets): reuse cached text and user preset data

This commit is contained in:
arnav-hiwarkar
2026-10-13 16:48:19 +05:30
parent 549e9d8251
commit da06216475
4 changed files with 209 additions and 16 deletions
+6 -3
View File
@@ -6,6 +6,7 @@ use effectcraft_project::build::{self, Ids};
use effectcraft_project::{Layer, LayerSource, Node, PropGroup};
use effectcraft_time::Tick;
use serde_json::{Value, json};
use std::sync::OnceLock;
use super::{CommandSpec, b_p, bad, has_comp, layer_mut, layer_p, str_p};
use crate::{Result, Session, cmd, query};
@@ -13,8 +14,10 @@ use crate::{Result, Session, cmd, query};
/// The original text animator presets (`presets/text_animators.json`).
pub const PRESETS_JSON: &str = include_str!("../../presets/text_animators.json");
fn presets() -> Vec<Value> {
serde_json::from_str::<Value>(PRESETS_JSON).ok().and_then(|v| v.get("presets").and_then(Value::as_array).cloned()).unwrap_or_default()
fn presets() -> &'static [Value] {
static PRESETS: OnceLock<Vec<Value>> = OnceLock::new();
PRESETS
.get_or_init(|| serde_json::from_str::<Value>(PRESETS_JSON).ok().and_then(|v| v.get("presets").and_then(Value::as_array).cloned()).unwrap_or_default())
}
/// (id, name) of every preset.
@@ -303,7 +306,7 @@ fn apply_preset(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid) = layer_p(s, p, C)?;
let want = str_p(p, "preset").ok_or_else(|| bad(C, "`preset` id or name required"))?.to_string();
let preset = presets()
.into_iter()
.iter()
.find(|x| x.get("id").and_then(Value::as_str) == Some(&want) || x.get("name").and_then(Value::as_str).is_some_and(|n| n.eq_ignore_ascii_case(&want)))
.ok_or_else(|| bad(C, format!("no preset {want}")))?;
let name = preset.get("name").and_then(Value::as_str).unwrap_or("Preset").to_string();
+58
View File
@@ -164,6 +164,64 @@ fn presets_apply_render_and_round_trip() {
assert!(matches(tg.sub("pathOptions").unwrap()).contains(&"forceAlignment".to_string()));
}
#[test]
fn cached_text_presets_keep_the_catalog_and_application_independent() {
let expected = [
("typewriter", "Typewriter"),
("fadeUpCharacters", "Fade Up Characters"),
("bounceInWords", "Bounce In Words"),
("trackingIn", "Tracking In"),
("scramble", "Scramble"),
("blurIn", "Blur In"),
("jitter", "Jitter"),
("dropInLines", "Drop In Lines"),
];
let (mut s, layer) = session();
let catalog = s.execute("text.presets", json!({})).unwrap();
let mut returned = catalog.clone();
returned[0]["name"] = json!("Edited copy");
let project = s.project.clone();
for _ in 0..2 {
let names = crate::text_presets();
assert_eq!(names.iter().map(|(id, name)| (id.as_str(), name.as_str())).collect::<Vec<_>>(), expected);
for (id, _) in expected {
let result = s.execute("layer.applyTextPreset", json!({"layer": layer, "preset": id})).unwrap();
assert!(!result["animators"].as_array().unwrap().is_empty(), "{id}");
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(s.project, project, "{id}: undo restores the document");
}
assert_eq!(s.execute("text.presets", json!({})).unwrap(), catalog);
}
}
#[test]
#[ignore = "warm text-preset listing benchmark"]
fn text_preset_listing_benchmark() {
let old = || {
let v: serde_json::Value = serde_json::from_str(include_str!("../presets/text_animators.json")).unwrap();
let presets = v["presets"].as_array().unwrap().clone();
presets.iter().map(|p| (p["id"].as_str().unwrap().to_string(), p["name"].as_str().unwrap().to_string())).collect::<Vec<_>>()
};
assert_eq!(old(), crate::text_presets());
let mut before = Vec::new();
let mut after = Vec::new();
for _ in 0..5 {
let start = std::time::Instant::now();
for _ in 0..1000 {
std::hint::black_box(old());
}
before.push(start.elapsed());
let start = std::time::Instant::now();
for _ in 0..1000 {
std::hint::black_box(crate::text_presets());
}
after.push(start.elapsed());
}
before.sort();
after.sort();
println!("Text preset names, median per warm lookup: {:?} -> {:?}", before[2] / 1000, after[2] / 1000);
}
#[test]
fn menus_have_text_animation_entries() {
let (s, _) = session();