font-store: index PostScript names shared by many files in linear time (#538)

Index::assemble searched every earlier position listed under a PostScript
name before adding the next, so an index that gives one name to many files
took time quadratic in their number. Files are visited in order, so a file
that lists a name twice can only be the last position pushed: comparing
with the end is enough.

find_postscript compared each hit's family name with every name collected
so far; it now keeps the lowercased names in a set.
This commit is contained in:
Dylan M. Taylor
2026-11-02 23:56:48 -05:00
committed by GitHub
parent 6ae033e6c1
commit 55332dd301
2 changed files with 26 additions and 3 deletions
+7 -3
View File
@@ -1,7 +1,7 @@
//! Font indexes (aiongg/craft-font-index, format 1.x): the families of every source in one
//! [`Index`], and exact lookup.
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, HashSet};
use serde::Deserialize;
@@ -519,8 +519,11 @@ impl Index {
by_name.entry(fam.family.to_lowercase()).or_default().push(fi);
for (xi, file) in fam.files.iter().enumerate() {
for ps in &file.postscript {
// Files are visited in order, so a file that lists a name twice can only
// have been the last one pushed: checking the end keeps this linear
// however many files share the name.
let hits = by_postscript.entry(ps.clone()).or_default();
if !hits.contains(&(fi, xi)) {
if hits.last() != Some(&(fi, xi)) {
hits.push((fi, xi));
}
}
@@ -610,9 +613,10 @@ impl Index {
pub fn find_postscript(&self, name: &str) -> Result<Face<'_>, Unavailable> {
let hits = self.by_postscript.get(name).map(Vec::as_slice).unwrap_or_default();
let mut names: Vec<&str> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for &(fi, _) in hits {
if let Some(f) = self.families.get(fi)
&& !names.iter().any(|n| n.to_lowercase() == f.family.to_lowercase())
&& seen.insert(f.family.to_lowercase())
{
names.push(&f.family);
}
+19
View File
@@ -206,6 +206,25 @@ fn postscript_lookups_match_exactly() {
assert!(index.family("Shared Two").unwrap().face_by_postscript("Shared-Regular").is_some());
}
#[test]
fn many_files_sharing_a_postscript_name_index_in_linear_time() {
let mut v = fixture().index;
let bytes = font("Many", "Regular", 400);
for f in 0..40 {
let files: Vec<serde_json::Value> = (0..2500)
.map(|i| file_entry(&format!("ofl/many{f}/Many-{i}.ttf"), &bytes, "normal", 400, json!([]), &["Many-Regular", "Many-Regular"]))
.collect();
let family = json!({"family": format!("Many {f}"), "license": "OFL", "license_path": null, "files": files});
v["families"].as_array_mut().unwrap().push(family);
}
let index = index_of(&v).unwrap();
let Err(Unavailable::Ambiguous { families, .. }) = index.find_postscript("Many-Regular") else { panic!("not ambiguous") };
assert_eq!(families.len(), 40);
assert_eq!(families[0], "Many 0");
let first = index.family("Many 7").unwrap();
assert_eq!(first.face_by_postscript("Many-Regular").unwrap().file_name(), "Many-0.ttf");
}
#[test]
fn styles_parse_and_urls_pin_the_commit() {
let index = index_of(&fixture().index).unwrap();