use std::collections::BTreeMap;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex, PoisonError};
use std::thread::available_parallelism;
use rayon::{ThreadPool, ThreadPoolBuilder};
static POOLS: Mutex<BTreeMap<NonZeroUsize, Option<Arc<ThreadPool>>>> = Mutex::new(BTreeMap::new());
#[cfg(feature = "fs")]
pub(crate) fn limit(threads: Option<NonZeroUsize>) -> usize {
threads.map_or_else(
|| available_parallelism().map_or(1, usize::from),
usize::from,
)
}
fn pool_size(threads: NonZeroUsize) -> NonZeroUsize {
available_parallelism().map_or(threads, |cores| threads.min(cores))
}
pub(crate) fn install<R: Send>(
threads: Option<NonZeroUsize>,
work: impl FnOnce() -> R + Send,
) -> R {
let Some(size) = threads.map(pool_size) else {
return work();
};
run_in(cached_pool(&POOLS, size, build_pool), work)
}
fn build_pool(size: NonZeroUsize) -> Option<ThreadPool> {
ThreadPoolBuilder::new()
.num_threads(size.get())
.thread_name(move |index| format!("webfont-{size}-{index}"))
.build()
.ok()
}
fn cached_pool(
pools: &Mutex<BTreeMap<NonZeroUsize, Option<Arc<ThreadPool>>>>,
size: NonZeroUsize,
build: impl FnOnce(NonZeroUsize) -> Option<ThreadPool>,
) -> Option<Arc<ThreadPool>> {
let cached = pools
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(&size)
.cloned();
cached.unwrap_or_else(|| {
let built = build(size).map(Arc::new);
if built.is_none() {
eprintln!(
"warning: webfont-generator: couldn't start {size} threads, so `threads: {size}` runs on the shared thread pool in this process"
);
}
pools
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry(size)
.or_insert(built)
.clone()
})
}
fn run_in<R: Send>(pool: Option<Arc<ThreadPool>>, work: impl FnOnce() -> R + Send) -> R {
match pool {
Some(pool) => pool.install(work),
None => work(),
}
}
#[cfg(test)]
pub(crate) static SINGLE_THREAD_PARSES: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) fn record_parse() {
if std::thread::current().name() == Some("webfont-1-0") {
SINGLE_THREAD_PARSES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cores() -> usize {
available_parallelism().unwrap().get()
}
#[test]
fn runs_inside_a_reused_pool_of_the_requested_size() {
let two = NonZeroUsize::new(2);
let pool = || {
let pools = POOLS.lock().unwrap();
Arc::as_ptr(pools[&pool_size(two.unwrap())].as_ref().unwrap())
};
assert_eq!(install(two, rayon::current_num_threads), 2.min(cores()));
let first = pool();
assert_eq!(
install(NonZeroUsize::new(3), rayon::current_num_threads),
3.min(cores())
);
install(two, || ());
assert_eq!(pool(), first);
}
#[test]
fn glyph_pools_stop_at_the_core_count() {
let many = NonZeroUsize::new(cores() + 5);
assert_eq!(install(many, rayon::current_num_threads), cores());
}
#[test]
fn a_refused_pool_warns_once_and_runs_on_the_caller() {
let pools = Mutex::new(BTreeMap::new());
let size = NonZeroUsize::new(2).unwrap();
let mut builds = 0;
assert!(
cached_pool(&pools, size, |_| {
builds += 1;
None
})
.is_none()
);
assert!(
cached_pool(&pools, size, |_| {
builds += 1;
None
})
.is_none()
);
assert_eq!(builds, 1, "a refused size isn't retried");
let caller = std::thread::current().id();
assert_eq!(run_in(None, || std::thread::current().id()), caller);
}
#[test]
fn unset_keeps_the_global_pool() {
assert_eq!(
install(None, rayon::current_num_threads),
rayon::current_num_threads()
);
}
#[test]
#[cfg(feature = "fs")]
fn reads_follow_the_requested_value_even_above_the_core_count() {
assert_eq!(limit(None), available_parallelism().map_or(1, usize::from));
assert_eq!(limit(NonZeroUsize::new(5)), 5);
assert_eq!(limit(NonZeroUsize::new(cores() + 5)), cores() + 5);
}
#[test]
#[cfg(all(feature = "fs", feature = "in_memory"))]
fn every_entry_point_runs_in_the_pool() {
use crate::{
FontType, FontVariant, GenerateWebfontsFromSourcesOptions, GenerateWebfontsOptions,
GlyphChange, RegenerationFiles, SvgSource, VariantFileSet,
};
use std::sync::atomic::Ordering::Relaxed;
struct Folder(std::path::PathBuf);
impl Drop for Folder {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
let folder = Folder(
std::env::temp_dir().join(format!("webfont-threads-paths-{}", std::process::id())),
);
std::fs::create_dir_all(&folder.0).unwrap();
let svg = |width: u32| {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0H{width}V24H0Z"/></svg>"#
)
};
let files: Vec<String> = ["a", "b"]
.iter()
.map(|name| {
let path = folder.0.join(format!("{name}.svg"));
std::fs::write(&path, svg(10)).unwrap();
path.to_string_lossy().into_owned()
})
.collect();
let one = NonZeroUsize::new(1);
let parses_in_pool = |step: &str, run: &mut dyn FnMut()| {
let before = SINGLE_THREAD_PARSES.load(Relaxed);
run();
assert!(SINGLE_THREAD_PARSES.load(Relaxed) > before, "{step}");
};
let options = || GenerateWebfontsOptions {
css: Some(false),
dest: folder.0.to_string_lossy().into_owned(),
files: files.clone(),
incremental: Some(true),
threads: one,
types: Some(vec![FontType::Ttf]),
write_files: Some(false),
..Default::default()
};
let edit = |width| std::fs::write(&files[1], svg(width)).unwrap();
let changed = [(files[1].clone(), GlyphChange::Changed { name: None })];
let mut result = None;
parses_in_pool("generate", &mut || {
result = Some(crate::generate_sync(options(), None).unwrap());
});
let mut result = result.unwrap();
parses_in_pool("regenerate", &mut || {
edit(12);
let single = RegenerationFiles::Single(files.clone());
result.regenerate(&single, &changed).unwrap();
});
parses_in_pool("regenerate_all", &mut || {
edit(14);
let single = RegenerationFiles::Single(files.clone());
result.regenerate_all(&single).unwrap();
});
let variant = |name: &str, weight, default| FontVariant {
name: name.to_owned(),
files: files.clone(),
weight: Some(weight),
default,
};
let mut family = None;
parses_in_pool("variant generate", &mut || {
let options = GenerateWebfontsOptions {
files: vec![],
types: Some(vec![FontType::Woff2]),
variants: Some(vec![
variant("light", 300, Some(true)),
variant("bold", 700, None),
]),
..options()
};
family = Some(crate::generate_sync(options, None).unwrap());
});
let mut family = family.unwrap();
parses_in_pool("variant regenerate", &mut || {
edit(16);
let sets = ["light", "bold"].map(|name| VariantFileSet {
variant: name.to_owned(),
files: files.clone(),
});
family
.regenerate(&RegenerationFiles::Variants(sets.into()), &changed)
.unwrap();
});
parses_in_pool("variant regenerate_all", &mut || {
edit(18);
let sets = ["light", "bold"].map(|name| VariantFileSet {
variant: name.to_owned(),
files: files.clone(),
});
family
.regenerate_all(&RegenerationFiles::Variants(sets.into()))
.unwrap();
});
let source = |svg: String| SvgSource {
id: "icon".to_owned(),
name: "icon".to_owned(),
svg,
};
let mut sources = None;
parses_in_pool("in-memory generate", &mut || {
sources = Some(
crate::generate_from_sources_sync(GenerateWebfontsFromSourcesOptions {
sources: vec![source(svg(10))],
incremental: Some(true),
threads: one,
types: Some(vec![FontType::Ttf]),
..Default::default()
})
.unwrap(),
);
});
let mut sources = sources.unwrap();
parses_in_pool("in-memory update", &mut || {
sources
.update_sync("icon".to_owned(), svg(12), None)
.unwrap();
});
}
}