perf(preview): cap the number of concurrent preview tokio tasks in the background (#67)

* perf(preview): cap the number of concurrent preview tokio tasks in the background

* perf(preview): check filesize asynchronously to avoid blocking the UI

* ci(changelog): fix action permissions
This commit is contained in:
Alexandre Pasmantier 2024-11-24 16:00:06 +01:00 committed by GitHub
parent dc36b2152d
commit 84d54b5751
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 127 additions and 95 deletions

View File

@ -1,9 +1,10 @@
name: changelog
permissions:
contents: write
pull_request: write
on:
pull_request:
push:
tags:
- '[v]?[0-9]+.[0-9]+.[0-9]+'

View File

@ -35,7 +35,7 @@ pub enum PreviewContent {
}
pub const PREVIEW_NOT_SUPPORTED_MSG: &str =
"Preview for this file type is not yet supported";
"Preview for this file type is not supported";
pub const FILE_TOO_LARGE_MSG: &str = "File too large";
/// A preview of an entry.

View File

@ -5,6 +5,7 @@ use parking_lot::Mutex;
use std::fs::File;
use std::io::{BufRead, BufReader, Seek};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use syntect::{
@ -28,6 +29,8 @@ pub struct FilePreviewer {
cache: Arc<Mutex<PreviewCache>>,
pub syntax_set: Arc<SyntaxSet>,
pub syntax_theme: Arc<Theme>,
concurrent_preview_tasks: Arc<AtomicU8>,
last_previewed: Arc<Mutex<Arc<Preview>>>,
//image_picker: Arc<Mutex<Picker>>,
}
@ -42,6 +45,12 @@ impl FilePreviewerConfig {
}
}
/// The maximum file size that we will try to preview.
/// 4 MB
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
const MAX_CONCURRENT_PREVIEW_TASKS: u8 = 2;
impl FilePreviewer {
pub fn new(config: Option<FilePreviewerConfig>) -> Self {
let hl_assets = load_highlighting_assets();
@ -62,14 +71,12 @@ impl FilePreviewer {
cache: Arc::new(Mutex::new(PreviewCache::default())),
syntax_set: Arc::new(syntax_set),
syntax_theme: Arc::new(theme),
concurrent_preview_tasks: Arc::new(AtomicU8::new(0)),
last_previewed: Arc::new(Mutex::new(Arc::new(Preview::default()))),
//image_picker: Arc::new(Mutex::new(image_picker)),
}
}
/// The maximum file size that we will try to preview.
/// 4 MB
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
/// Get a preview for a file entry.
///
/// # Panics
@ -83,44 +90,32 @@ impl FilePreviewer {
}
debug!("No preview in cache for {:?}", entry.name);
// check file size
if get_file_size(&path_buf).map_or(false, |s| s > Self::MAX_FILE_SIZE)
if self.concurrent_preview_tasks.load(Ordering::Relaxed)
< MAX_CONCURRENT_PREVIEW_TASKS
{
debug!("File too large: {:?}", entry.name);
let preview = meta::file_too_large(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone());
return preview;
self.concurrent_preview_tasks
.fetch_add(1, Ordering::Relaxed);
let cache = self.cache.clone();
let entry_c = entry.clone();
let syntax_set = self.syntax_set.clone();
let syntax_theme = self.syntax_theme.clone();
let path = path_buf.clone();
let concurrent_tasks = self.concurrent_preview_tasks.clone();
let last_previewed = self.last_previewed.clone();
tokio::spawn(async move {
try_preview(
entry_c,
path,
cache,
syntax_set,
syntax_theme,
concurrent_tasks,
last_previewed,
);
});
}
// try to determine file type
debug!("Computing preview for {:?}", entry.name);
if let FileType::Text = FileType::from(&path_buf) {
debug!("File is text-based: {:?}", entry.name);
match File::open(&path_buf) {
Ok(file) => {
// insert a loading preview into the cache
let preview = meta::loading(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone());
// compute the highlighted version in the background
let mut reader = BufReader::new(file);
reader.seek(std::io::SeekFrom::Start(0)).unwrap();
self.compute_highlighted_text_preview(entry, reader);
preview
}
Err(e) => {
warn!("Error opening file: {:?}", e);
let p = meta::not_supported(&entry.name);
self.cache_preview(entry.name.clone(), p.clone());
p
}
}
} else {
debug!("File isn't text-based: {:?}", entry.name);
let preview = meta::not_supported(&entry.name);
self.cache_preview(entry.name.clone(), preview.clone());
preview
}
self.last_previewed.lock().clone()
}
//async fn compute_image_preview(&self, entry: &entry::Entry) {
@ -145,65 +140,101 @@ impl FilePreviewer {
// });
//}
fn compute_highlighted_text_preview(
&self,
entry: &entry::Entry,
reader: BufReader<File>,
) {
let cache = self.cache.clone();
let syntax_set = self.syntax_set.clone();
let syntax_theme = self.syntax_theme.clone();
let entry_c = entry.clone();
tokio::spawn(async move {
debug!(
"Computing highlights in the background for {:?}",
entry_c.name
);
let lines: Vec<String> = reader
.lines()
.map_while(Result::ok)
// we need to add a newline here because sublime syntaxes expect one
// to be present at the end of each line
.map(|line| preprocess_line(&line).0 + "\n")
.collect();
match syntax::compute_highlights_for_path(
&PathBuf::from(&entry_c.name),
lines,
&syntax_set,
&syntax_theme,
) {
Ok(highlighted_lines) => {
debug!(
"Successfully computed highlights for {:?}",
entry_c.name
);
cache.lock().insert(
entry_c.name.clone(),
Arc::new(Preview::new(
entry_c.name,
PreviewContent::SyntectHighlightedText(
highlighted_lines,
),
entry_c.icon,
)),
);
debug!("Inserted highlighted preview into cache");
}
Err(e) => {
warn!("Error computing highlights: {:?}", e);
let preview = meta::not_supported(&entry_c.name);
cache.lock().insert(entry_c.name.clone(), preview);
}
};
});
}
#[allow(dead_code)]
fn cache_preview(&mut self, key: String, preview: Arc<Preview>) {
self.cache.lock().insert(key, preview);
}
}
pub fn try_preview(
entry: entry::Entry,
path: PathBuf,
cache: Arc<Mutex<PreviewCache>>,
syntax_set: Arc<SyntaxSet>,
syntax_theme: Arc<Theme>,
concurrent_tasks: Arc<AtomicU8>,
last_previewed: Arc<Mutex<Arc<Preview>>>,
) {
debug!("Computing preview for {:?}", entry.name);
tokio::spawn(async move {
// check file size
if get_file_size(&path).map_or(false, |s| s > MAX_FILE_SIZE) {
debug!("File too large: {:?}", entry.name);
let preview = meta::file_too_large(&entry.name);
cache.lock().insert(entry.name.clone(), preview.clone());
}
if matches!(FileType::from(&path), FileType::Text) {
debug!("File is text-based: {:?}", entry.name);
match File::open(&path) {
Ok(file) => {
// compute the highlighted version in the background
let mut reader = BufReader::new(file);
reader.seek(std::io::SeekFrom::Start(0)).unwrap();
let preview = compute_highlighted_text_preview(
&entry,
reader,
&syntax_set,
&syntax_theme,
);
cache.lock().insert(entry.name.clone(), preview.clone());
let mut tp = last_previewed.lock();
*tp = preview;
}
Err(e) => {
warn!("Error opening file: {:?}", e);
let p = meta::not_supported(&entry.name);
cache.lock().insert(entry.name.clone(), p.clone());
}
}
} else {
debug!("File isn't text-based: {:?}", entry.name);
let preview = meta::not_supported(&entry.name);
cache.lock().insert(entry.name.clone(), preview.clone());
}
concurrent_tasks.fetch_sub(1, Ordering::Relaxed);
});
}
fn compute_highlighted_text_preview(
entry: &entry::Entry,
reader: BufReader<File>,
syntax_set: &SyntaxSet,
syntax_theme: &Theme,
) -> Arc<Preview> {
debug!(
"Computing highlights in the background for {:?}",
entry.name
);
let lines: Vec<String> = reader
.lines()
.map_while(Result::ok)
// we need to add a newline here because sublime syntaxes expect one
// to be present at the end of each line
.map(|line| preprocess_line(&line).0 + "\n")
.collect();
match syntax::compute_highlights_for_path(
&PathBuf::from(&entry.name),
lines,
syntax_set,
syntax_theme,
) {
Ok(highlighted_lines) => {
debug!("Successfully computed highlights for {:?}", entry.name);
Arc::new(Preview::new(
entry.name.clone(),
PreviewContent::SyntectHighlightedText(highlighted_lines),
entry.icon,
))
}
Err(e) => {
warn!("Error computing highlights: {:?}", e);
meta::not_supported(&entry.name)
}
}
}
//fn get_image_picker() -> Picker {
// let mut picker = match Picker::from_termios() {
// Ok(p) => p,