Cache fonts and file reads to speed up compiles
This commit is contained in:
@@ -245,7 +245,7 @@ pub fn asset_files(app: &AppHandle, store: &Store) -> HashMap<String, Vec<u8>> {
|
|||||||
if name.starts_with('.') {
|
if name.starts_with('.') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Ok(data) = std::fs::read(entry.path()) {
|
if let Some(data) = crate::workspace::read_file_cached(&entry.path()) {
|
||||||
files.insert(name, data);
|
files.insert(name, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-11
@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
use std::time::SystemTime;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
use crate::db::Store;
|
use crate::db::Store;
|
||||||
@@ -25,6 +27,34 @@ pub fn content_hash(bytes: &[u8]) -> String {
|
|||||||
format!("{:x}", Sha256::digest(bytes))
|
format!("{:x}", Sha256::digest(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_file_cached(path: &Path) -> Option<Vec<u8>> {
|
||||||
|
type Cache = Mutex<HashMap<PathBuf, (SystemTime, u64, Arc<Vec<u8>>)>>;
|
||||||
|
static CACHE: OnceLock<Cache> = OnceLock::new();
|
||||||
|
|
||||||
|
let metadata = std::fs::metadata(path).ok()?;
|
||||||
|
let modified = metadata.modified().ok()?;
|
||||||
|
let size = metadata.len();
|
||||||
|
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
if let Ok(map) = cache.lock() {
|
||||||
|
if let Some((stamp, cached_size, bytes)) = map.get(path) {
|
||||||
|
if *stamp == modified && *cached_size == size {
|
||||||
|
return Some(bytes.as_ref().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = std::fs::read(path).ok()?;
|
||||||
|
if let Ok(mut map) = cache.lock() {
|
||||||
|
map.insert(
|
||||||
|
path.to_path_buf(),
|
||||||
|
(modified, size, Arc::new(bytes.clone())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub workspace_root: String,
|
pub workspace_root: String,
|
||||||
@@ -128,8 +158,6 @@ pub struct BrowseEntry {
|
|||||||
pub last_synced_at: Option<String>,
|
pub last_synced_at: Option<String>,
|
||||||
pub child_count: usize,
|
pub child_count: usize,
|
||||||
pub cloud_linked: bool,
|
pub cloud_linked: bool,
|
||||||
/// "synced" when nothing changed since the last sync, "pending" when local
|
|
||||||
/// edits are waiting to go up, or None when the entry is not linked.
|
|
||||||
pub sync_state: Option<String>,
|
pub sync_state: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +189,6 @@ fn newest_change(dir: &Path) -> Option<chrono::DateTime<chrono::Utc>> {
|
|||||||
newest
|
newest
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compares when the entry last changed on disk against when it was last
|
|
||||||
/// synced. Metadata only, so browsing stays cheap.
|
|
||||||
pub fn sync_state_of(path: &Path, synced_at: Option<&str>) -> Option<String> {
|
pub fn sync_state_of(path: &Path, synced_at: Option<&str>) -> Option<String> {
|
||||||
sync_state_for(modified_time(path), synced_at)
|
sync_state_for(modified_time(path), synced_at)
|
||||||
}
|
}
|
||||||
@@ -257,8 +283,6 @@ pub fn browse(app: &AppHandle, store: &Store, relative: &str) -> Result<Vec<Brow
|
|||||||
let kind = if is_typst_file(&name) { "document" } else { "file" };
|
let kind = if is_typst_file(&name) { "document" } else { "file" };
|
||||||
let link = store.document_link(&path)?;
|
let link = store.document_link(&path)?;
|
||||||
|
|
||||||
// Cloud documents are managed from the Cloud view, so they are not
|
|
||||||
// listed a second time here.
|
|
||||||
if link.is_some() {
|
if link.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -379,7 +403,7 @@ fn collect_loose_files(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
collect_loose_files(&path, &key, map)?;
|
collect_loose_files(&path, &key, map)?;
|
||||||
} else if let Ok(bytes) = std::fs::read(&path) {
|
} else if let Some(bytes) = read_file_cached(&path) {
|
||||||
map.insert(key, bytes);
|
map.insert(key, bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,9 +464,10 @@ pub fn read_all_files(project_dir: &Path) -> Result<HashMap<String, Vec<u8>>, St
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for relative in collect_files(project_dir)? {
|
for relative in collect_files(project_dir)? {
|
||||||
let full = project_file_path(project_dir, &relative)?;
|
let full = project_file_path(project_dir, &relative)?;
|
||||||
let bytes = std::fs::read(&full).map_err(|e| e.to_string())?;
|
if let Some(bytes) = read_file_cached(&full) {
|
||||||
map.insert(relative, bytes);
|
map.insert(relative, bytes);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,9 +480,6 @@ pub struct FileEntry {
|
|||||||
pub size: u64,
|
pub size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lists a project's contents for the editor tree. Unlike `collect_files`,
|
|
||||||
/// which feeds sync and only cares about file contents, this includes
|
|
||||||
/// directories so an empty folder is still visible after it is created.
|
|
||||||
pub fn list_files(project_dir: &Path) -> Result<Vec<FileEntry>, String> {
|
pub fn list_files(project_dir: &Path) -> Result<Vec<FileEntry>, String> {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
|||||||
+39
-14
@@ -1,5 +1,6 @@
|
|||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use typst::diag::{FileError, FileResult};
|
use typst::diag::{FileError, FileResult};
|
||||||
use typst::foundations::{Bytes, Datetime, Duration};
|
use typst::foundations::{Bytes, Datetime, Duration};
|
||||||
@@ -23,6 +24,37 @@ fn normalize_path(path: &str) -> String {
|
|||||||
path.trim_start_matches('/').replace('\\', "/")
|
path.trim_start_matches('/').replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bundled_fonts() -> &'static Vec<Font> {
|
||||||
|
static FONTS: OnceLock<Vec<Font>> = OnceLock::new();
|
||||||
|
FONTS.get_or_init(|| {
|
||||||
|
let mut fonts = Vec::new();
|
||||||
|
for data in typst_assets::fonts() {
|
||||||
|
fonts.extend(Font::iter(Bytes::new(data)));
|
||||||
|
}
|
||||||
|
fonts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_fonts(name: &str, data: &[u8]) -> Vec<Font> {
|
||||||
|
static CACHE: OnceLock<Mutex<HashMap<(String, usize), Vec<Font>>>> = OnceLock::new();
|
||||||
|
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
||||||
|
let key = (name.to_string(), data.len());
|
||||||
|
|
||||||
|
if let Ok(map) = cache.lock() {
|
||||||
|
if let Some(fonts) = map.get(&key) {
|
||||||
|
return fonts.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fonts: Vec<Font> = Font::iter(Bytes::new(data.to_vec())).collect();
|
||||||
|
|
||||||
|
if let Ok(mut map) = cache.lock() {
|
||||||
|
map.insert(key, fonts.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fonts
|
||||||
|
}
|
||||||
|
|
||||||
impl ProjectWorld {
|
impl ProjectWorld {
|
||||||
pub fn new(entrypoint: String, files: HashMap<String, Vec<u8>>, enable_html: bool) -> Self {
|
pub fn new(entrypoint: String, files: HashMap<String, Vec<u8>>, enable_html: bool) -> Self {
|
||||||
let main = FileId::new(RootedPath::new(
|
let main = FileId::new(RootedPath::new(
|
||||||
@@ -33,16 +65,7 @@ impl ProjectWorld {
|
|||||||
let downloader = SystemDownloader::new("TypstDesktop (typst-kit)");
|
let downloader = SystemDownloader::new("TypstDesktop (typst-kit)");
|
||||||
let packages = SystemPackages::new(downloader);
|
let packages = SystemPackages::new(downloader);
|
||||||
|
|
||||||
let mut book = FontBook::new();
|
let mut fonts = bundled_fonts().clone();
|
||||||
let mut fonts = Vec::new();
|
|
||||||
|
|
||||||
for data in typst_assets::fonts() {
|
|
||||||
let buffer = Bytes::new(data);
|
|
||||||
for font in Font::iter(buffer) {
|
|
||||||
book.push(font.info().clone());
|
|
||||||
fonts.push(font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name, data) in &files {
|
for (name, data) in &files {
|
||||||
let lower = name.to_lowercase();
|
let lower = name.to_lowercase();
|
||||||
@@ -50,11 +73,13 @@ impl ProjectWorld {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|ext| lower.ends_with(ext))
|
.any(|ext| lower.ends_with(ext))
|
||||||
{
|
{
|
||||||
for font in Font::iter(Bytes::new(data.clone())) {
|
fonts.extend(custom_fonts(name, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut book = FontBook::new();
|
||||||
|
for font in &fonts {
|
||||||
book.push(font.info().clone());
|
book.push(font.info().clone());
|
||||||
fonts.push(font);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let library = if enable_html {
|
let library = if enable_html {
|
||||||
|
|||||||
Reference in New Issue
Block a user