mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
perf(format): index the lookups that made style-heavy files quadratic
REST import searched every style imported so far for each layer's style reference; .fig export compared each text span's style with every distinct style before it; Penpot token-set ordering searched the whole order array for every comparison of the sort. Each now looks up a set, a map or a position table built once.
This commit is contained in:
1 parent
02c38a6d95
commit
37803cb1c8
5 files changed
+58
-7
No files matched your search
@@ -56,16 +56,24 @@ impl<'d> Exporter<'d> {
|
||||
}
|
||||
let spans = td.spans();
|
||||
let mut table: Vec<&TextStyle> = Vec::new();
|
||||
// Each distinct style once, found by its serialized form rather than by comparing it
|
||||
// with every earlier one (quadratic in the number of distinct styles).
|
||||
let mut index: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
|
||||
let mut span_ids = Vec::with_capacity(spans.len());
|
||||
for (_, _, st) in &spans {
|
||||
span_ids.push(if **st == td.style {
|
||||
0
|
||||
} else {
|
||||
let i = table.iter().position(|t| *t == *st).unwrap_or_else(|| {
|
||||
table.push(st);
|
||||
table.len() - 1
|
||||
});
|
||||
i as u32 + 1
|
||||
match serde_json::to_string(st) {
|
||||
Ok(key) => *index.entry(key).or_insert_with(|| {
|
||||
table.push(st);
|
||||
table.len() as u32
|
||||
}),
|
||||
Err(_) => {
|
||||
table.push(st);
|
||||
table.len() as u32
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let mut chars = String::new();
|
||||
|
||||
@@ -1611,3 +1611,19 @@ fn a_whole_ellipse_starting_anywhere_comes_back_whole() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_with_thousands_of_distinct_styles_exports_in_linear_time() {
|
||||
// The style table used to be searched end to end for every span (quadratic in distinct styles).
|
||||
let mut doc = Document::new("styles");
|
||||
let p = doc.first_page().unwrap();
|
||||
let mut td = TextData::new("x".repeat(20_000));
|
||||
td.runs = (0..20_000)
|
||||
.map(|i| StyleRun { start: i, end: i + 1, style: TextStyle { font_size: 8.0 + i as f64 / 100.0, ..TextStyle::default() } })
|
||||
.collect();
|
||||
doc.create(p, NodeKind::Text(td), "T", 0.0, 0.0, 100.0, 100.0).unwrap();
|
||||
let start = std::time::Instant::now();
|
||||
let (bytes, _) = export(&doc, None).unwrap();
|
||||
assert!(!bytes.is_empty());
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed());
|
||||
}
|
||||
@@ -2131,7 +2131,10 @@ fn import_themed_tokens(
|
||||
if set_names.is_empty() {
|
||||
return false;
|
||||
}
|
||||
set_names.sort_by_key(|name| order.iter().position(|o| o == name).unwrap_or(usize::MAX));
|
||||
// Each set's place in the order once: a position search per comparison was quadratic in
|
||||
// the order's length for every one of the sort's comparisons.
|
||||
let place: BTreeMap<&str, usize> = order.iter().enumerate().map(|(i, o)| (*o, i)).collect();
|
||||
set_names.sort_by_key(|name| place.get(name).copied().unwrap_or(usize::MAX));
|
||||
let set_names: Vec<&str> = set_names.into_iter().take(512).collect();
|
||||
let mut tokens: BTreeMap<&str, BTreeMap<String, &Value>> = BTreeMap::new();
|
||||
for name in &set_names {
|
||||
|
||||
@@ -427,6 +427,9 @@ pub(crate) struct Ctx<'a> {
|
||||
component_meta: BTreeMap<&'a str, &'a Value>,
|
||||
set_meta: BTreeMap<&'a str, &'a Value>,
|
||||
style_meta: BTreeMap<&'a str, &'a Value>,
|
||||
/// The ids of the styles imported so far (`doc.styles` is a Vec: a lookup per reference
|
||||
/// would be quadratic in a document with many distinct references).
|
||||
style_ids: BTreeSet<String>,
|
||||
/// Instances (imported as frames until their main is resolved) with their JSON.
|
||||
instances: Vec<(NodeId, &'a Value)>,
|
||||
/// `SLOT` layers (imported as frames until their slot property is resolved) with their JSON.
|
||||
@@ -468,6 +471,7 @@ impl<'a> Ctx<'a> {
|
||||
component_meta: BTreeMap::new(),
|
||||
set_meta: BTreeMap::new(),
|
||||
style_meta: BTreeMap::new(),
|
||||
style_ids: BTreeSet::new(),
|
||||
instances: Vec::new(),
|
||||
slots: Vec::new(),
|
||||
slot_defaults: BTreeMap::new(),
|
||||
@@ -941,6 +945,7 @@ impl<'a> Ctx<'a> {
|
||||
};
|
||||
self.bindings(&mut sample, node);
|
||||
let bound = sample.bound.iter().filter_map(|(k, v)| Some((style_binding(slot, k)?, v.clone()))).collect();
|
||||
self.style_ids.insert(id.to_string());
|
||||
self.doc.styles.push(Style { id: id.to_string(), name, description, kind, bound });
|
||||
}
|
||||
}
|
||||
@@ -961,13 +966,14 @@ impl<'a> Ctx<'a> {
|
||||
_ => continue,
|
||||
};
|
||||
*target = Some(sid.to_string());
|
||||
if self.doc.styles.iter().any(|s| s.id == sid) {
|
||||
if self.style_ids.contains(sid) {
|
||||
continue;
|
||||
}
|
||||
let meta = self.style_meta.get(sid);
|
||||
let name =
|
||||
meta.and_then(|m| text(m, "name")).filter(|s| !s.is_empty()).map_or_else(|| format!("Style {sid}"), |s| clip_str(s).to_string());
|
||||
let description = meta.and_then(|m| text(m, "description")).map(|s| clip_str(s).to_string()).unwrap_or_default();
|
||||
self.style_ids.insert(sid.to_string());
|
||||
self.doc.styles.push(Style { id: sid.to_string(), name, description, kind, bound: BTreeMap::new() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,3 +75,21 @@ fn stroke_colour_variable_on_outlined_vector_is_kept() {
|
||||
icon.bound
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thousands_of_distinct_style_references_import_in_linear_time() {
|
||||
// Each reference used to search every style imported so far (quadratic in their number).
|
||||
let fill = json!({"type": "SOLID", "color": {"r": 1, "g": 0, "b": 0, "a": 1}});
|
||||
let n = 20_000;
|
||||
let children: Vec<Value> = (0..n)
|
||||
.map(|i| {
|
||||
json!({"id": format!("1:{i}"), "name": "R", "type": "RECTANGLE", "relativeTransform": at(0.0, 0.0), "size": {"x": 10, "y": 10},
|
||||
"fills": [fill], "styles": {"fill": format!("S:{i}")}})
|
||||
})
|
||||
.collect();
|
||||
let v = file(Value::Array(children));
|
||||
let start = std::time::Instant::now();
|
||||
let (doc, _) = import(&v, &no_images).unwrap();
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed());
|
||||
assert_eq!(doc.styles.len(), n);
|
||||
}
|
||||
Reference in new issue
Block a user