boost startup performance more

performance is now on-par with wofi
This commit is contained in:
Alexander Mohr 2025-04-25 20:21:35 +02:00
parent 8673ef4d13
commit 67a77e49f2
5 changed files with 130 additions and 94 deletions

View file

@ -405,7 +405,7 @@ impl Default for Config {
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_show_animation_time() -> Option<u64> {
Some(50)
Some(10)
}
// allowed because option is needed for serde macro

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;
use std::{env, fs, string};
use freedesktop_file_parser::DesktopFile;
@ -112,6 +113,7 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<Pa
fs::read_dir(folder).ok().map(|entries| {
entries
.filter_map(Result::ok)
.par_bridge() // Convert to parallel iterator
.filter_map(|entry| entry.path().canonicalize().ok())
.filter(|entry| {
entry
@ -135,6 +137,8 @@ pub fn find_desktop_files() -> Vec<DesktopFile> {
PathBuf::from("/var/lib/flatpak/exports/share/applications"),
];
let start = Instant::now();
if let Some(home) = dirs::home_dir() {
paths.push(home.join(".local/share/applications"));
}
@ -163,6 +167,7 @@ pub fn find_desktop_files() -> Vec<DesktopFile> {
})
})
.collect();
log::debug!("Found {} desktop files in {:?}", p.len(), start.elapsed());
p
}

View file

@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use crate::config::{Anchor, Config, MatchMethod, WrapMode};
@ -31,7 +32,7 @@ type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>;
type ArcProvider<T> = Arc<Mutex<dyn ItemProvider<T>>>;
type MenuItemSender<T> = Sender<Result<MenuItem<T>, anyhow::Error>>;
pub trait ItemProvider<T: std::clone::Clone> {
pub trait ItemProvider<T: Clone> {
fn get_elements(&mut self, search: Option<&str>) -> Vec<MenuItem<T>>;
fn get_sub_elements(&mut self, item: &MenuItem<T>) -> Option<Vec<MenuItem<T>>>;
}
@ -102,9 +103,10 @@ impl<T: Clone> AsRef<MenuItem<T>> for MenuItem<T> {
/// Will return Err when the channel between the UI and this is broken
pub fn show<T, P>(config: Config, item_provider: P) -> Result<MenuItem<T>, anyhow::Error>
where
T: Clone + 'static,
P: ItemProvider<T> + 'static + Clone,
T: Clone + 'static + std::marker::Send,
P: ItemProvider<T> + 'static + Clone + std::marker::Send,
{
log::debug!("Starting GUI");
if let Some(ref css) = config.style {
let provider = CssProvider::new();
let css_file_path = File::for_path(css);
@ -136,19 +138,45 @@ fn build_ui<T, P>(
sender: &Sender<Result<MenuItem<T>, anyhow::Error>>,
app: &Application,
) where
T: Clone + 'static,
P: ItemProvider<T> + 'static,
T: Clone + 'static + std::marker::Send,
P: ItemProvider<T> + 'static + std::marker::Send,
{
let start = Instant::now();
let item_provider = Arc::new(Mutex::new(item_provider));
let provider_clone = Arc::clone(&item_provider);
let get_items = thread::spawn(move || {
log::debug!("getting items");
provider_clone.lock().unwrap().get_elements(None)
});
let window = ApplicationWindow::builder()
.application(app)
.decorated(false)
.resizable(false)
.default_width(0)
.default_height(0)
.default_width(100)
.default_height(100)
.build();
let window_show = Instant::now();
let entry = SearchEntry::new();
let inner_box = FlowBox::new();
let list_items: ArcMenuMap<T> = Arc::new(Mutex::new(HashMap::new()));
// handle keys as soon as possible
setup_key_event_handler(
&window,
&entry,
inner_box.clone(),
app.clone(),
sender.clone(),
ArcMenuMap::clone(&list_items),
config.clone(),
item_provider,
);
log::debug!("keyboard ready after {:?}", start.elapsed());
window.set_widget_name("window");
if !config.normal_window {
@ -159,6 +187,8 @@ fn build_ui<T, P>(
window.set_namespace(Some("worf"));
}
let window_done = Instant::now();
if let Some(location) = config.location.as_ref() {
for anchor in location {
window.set_anchor(anchor.into(), true);
@ -168,7 +198,6 @@ fn build_ui<T, P>(
let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0);
outer_box.set_widget_name("outer-box");
let entry = SearchEntry::new();
entry.set_widget_name("input");
entry.set_css_classes(&["input"]);
entry.set_placeholder_text(config.prompt.as_deref());
@ -186,16 +215,18 @@ fn build_ui<T, P>(
outer_box.append(&scroll);
let inner_box = FlowBox::new();
inner_box.set_widget_name("inner-box");
inner_box.set_css_classes(&["inner-box"]);
inner_box.set_hexpand(true);
inner_box.set_vexpand(false);
inner_box.set_selection_mode(gtk4::SelectionMode::Browse);
inner_box.set_max_children_per_line(config.columns.unwrap());
inner_box.set_activate_on_single_click(true);
if let Some(halign) = config.halign {
inner_box.set_halign(halign.into());
}
if let Some(valign) = config.valign {
inner_box.set_valign(valign.into());
} else if config.orientation.unwrap() == config::Orientation::Horizontal {
@ -204,28 +235,6 @@ fn build_ui<T, P>(
inner_box.set_valign(Align::Start);
}
inner_box.set_selection_mode(gtk4::SelectionMode::Browse);
inner_box.set_max_children_per_line(config.columns.unwrap());
inner_box.set_activate_on_single_click(true);
let item_provider = Arc::new(Mutex::new(item_provider));
let list_items: ArcMenuMap<T> = Arc::new(Mutex::new(HashMap::new()));
let elements = item_provider.lock().unwrap().get_elements(None);
build_ui_from_menu_items(
&elements,
&list_items,
&inner_box,
config,
sender,
app,
&window,
);
let items_sort = ArcMenuMap::clone(&list_items);
inner_box
.set_sort_func(move |child1, child2| sort_menu_items_by_score(child1, child2, &items_sort));
let items_focus = ArcMenuMap::clone(&list_items);
inner_box.connect_map(move |fb| {
fb.grab_focus();
@ -238,23 +247,25 @@ fn build_ui<T, P>(
wrapper_box.append(&inner_box);
scroll.set_child(Some(&wrapper_box));
setup_key_event_handler(
&window,
&entry,
inner_box,
app.clone(),
sender.clone(),
ArcMenuMap::clone(&list_items),
config.clone(),
item_provider,
);
window.set_child(Some(&outer_box));
let window_show = Instant::now();
window.show();
let window_done = Instant::now();
let wait_for_items = Instant::now();
let elements = get_items.join().unwrap();
log::debug!("got items after {:?}", wait_for_items.elapsed());
build_ui_from_menu_items(
&elements,
&list_items,
&inner_box,
config,
sender,
app,
&window,
);
let items_sort = ArcMenuMap::clone(&list_items);
inner_box
.set_sort_func(move |child1, child2| sort_menu_items_by_score(child1, child2, &items_sort));
window.show();
animate_window_show(config, window.clone());
let animation_done = Instant::now();
@ -313,7 +324,7 @@ fn build_ui_from_menu_items<T: Clone + 'static>(
}
#[allow(clippy::too_many_arguments)] // todo fix this
fn setup_key_event_handler<T: Clone + 'static>(
fn setup_key_event_handler<T: Clone + 'static + Send>(
window: &ApplicationWindow,
entry: &SearchEntry,
inner_box: FlowBox,
@ -477,13 +488,13 @@ fn sort_menu_items_by_score<T: std::clone::Clone>(
}
fn animate_window_show(config: &Config, window: ApplicationWindow) {
let display = window.display();
if let Some(surface) = window.surface() {
let display = window.display();
// todo this does not work for multi monitor systems
let monitor = display.monitor_at_surface(&surface);
if let Some(monitor) = monitor {
let geometry = monitor.geometry();
log::debug!("monitor geometry: {:?}", geometry);
let Some(target_width) = percent_or_absolute(config.width.as_ref(), geometry.width())
else {
return;
@ -495,12 +506,19 @@ fn animate_window_show(config: &Config, window: ApplicationWindow) {
return;
};
log::debug!(
"monitor geometry: {geometry:?}, target_height {target_height}, target_width {target_width}"
);
let animation_start = Instant::now();
animate_window(
window.clone(),
config.show_animation_time.unwrap_or(0),
target_height,
target_width,
move || {},
move || {
log::debug!("animation done after {:?}", animation_start.elapsed());
},
);
}
}
@ -522,10 +540,10 @@ where
}
fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
if t < 0.7 {
10.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
1.0 - (-2.0 * t + 2.0).powi(3)
}
}
@ -548,7 +566,7 @@ fn animate_window<Func>(
let allocation = window.allocation();
// Define animation parameters
let animation_step_length = Duration::from_millis(8); // ~120 FPS
let animation_step_length = Duration::from_millis(20);
// Start positions (initial window dimensions)
let start_width = allocation.width() as f32;
@ -558,12 +576,23 @@ fn animate_window<Func>(
let delta_width = target_width as f32 - start_width;
let delta_height = target_height as f32 - start_height;
// Start the animation timer
let start_time = Instant::now();
// Animation time starts when the timeout is ran for the first time
let mut start_time: Option<Instant> = None;
let mut last_t = 0.0;
let before_animation = Instant::now();
timeout_add_local(animation_step_length, move || {
if !window.is_visible() {
return ControlFlow::Continue;
}
let start_time = start_time.unwrap_or_else(|| {
let now = Instant::now();
start_time = Some(now);
log::debug!("animation started after {:?}", before_animation.elapsed());
now
});
let elapsed_us = start_time.elapsed().as_micros() as f32;
let t = (elapsed_us / (animation_time * 1000) as f32).min(1.0);
@ -746,8 +775,6 @@ fn create_menu_row<T: Clone + 'static>(
row_box.set_halign(Align::Fill);
row.set_child(Some(&row_box));
let ui_created = Instant::now();
if config.allow_images.is_some_and(|allow_images| allow_images) {
if let Some(image) = lookup_icon(&menu_item, config) {
image.set_widget_name("img");
@ -755,8 +782,6 @@ fn create_menu_row<T: Clone + 'static>(
}
}
let icon_found = Instant::now();
let label = Label::new(Some(menu_item.label.as_str()));
let wrap_mode: NaturalWrapMode = if let Some(config_wrap) = &config.line_wrap {
config_wrap.into()
@ -780,23 +805,18 @@ fn create_menu_row<T: Clone + 'static>(
label.set_xalign(0.0);
}
// log::debug!(
// "Creating menu took {:?}, ui created after {:?}, icon found after {:?}",
// start.elapsed(),
// ui_created - start,
// icon_found - start
// );
row.upcast()
}
fn lookup_icon<T: Clone>(menu_item: &MenuItem<T>, config: &Config) -> Option<Image> {
if let Some(image_path) = &menu_item.icon_path {
let img_regex = Regex::new(&format!(
r"((?i).*{})|(^/.*)",
r"((?i).*{})",
known_image_extension_regex_pattern()
));
let image = if img_regex.unwrap().is_match(image_path) {
let image = if image_path.starts_with("/") {
Image::from_file(image_path)
} else if img_regex.unwrap().is_match(image_path) {
if let Ok(img) = desktop::fetch_icon_from_common_dirs(&image_path) {
Image::from_file(img)
} else {

View file

@ -7,7 +7,7 @@ use freedesktop_file_parser::EntryType;
use rayon::prelude::*;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::io::Read;
use std::os::unix::prelude::CommandExt;
use std::path::{Path, PathBuf};
@ -46,22 +46,34 @@ struct DRunCache {
#[derive(Clone)]
struct DRunProvider<T: Clone> {
items: Vec<MenuItem<T>>,
items: Option<Vec<MenuItem<T>>>,
cache_path: Option<PathBuf>,
cache: HashMap<String, i64>,
data: T,
}
impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
fn new(menu_item_data: T) -> Self {
let (cache_path, d_run_cache) = load_d_run_cache();
DRunProvider {
items: None,
cache_path,
cache: d_run_cache,
data: menu_item_data,
}
}
fn load(&self) -> Vec<MenuItem<T>> {
let locale_variants = get_locale_variants();
let default_icon = "application-x-executable".to_string();
let (cache_path, d_run_cache) = load_d_run_cache();
let start = Instant::now();
let mut entries: Vec<MenuItem<T>> = find_desktop_files()
let entries: Vec<MenuItem<T>> = find_desktop_files()
.into_par_iter()
.filter(|file| {
!file.entry.no_display.unwrap_or(false)
&& !file.entry.hidden.unwrap_or(false)
})
.filter_map(|file| {
let name = lookup_name_with_locale(
&locale_variants,
@ -85,9 +97,7 @@ impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
.unwrap_or(false);
if !cmd_exists {
log::warn!(
"Skipping desktop entry for {name:?} because action {action:?} does not exist"
);
log::warn!("Skipping desktop entry for {name:?} because action {action:?} does not exist");
return None;
}
@ -98,9 +108,7 @@ impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
.map(|s| s.content.clone())
.or(Some(default_icon.clone()));
log::debug!("file, name={name:?}, icon={icon:?}, action={action:?}");
let sort_score = *d_run_cache.get(&name).unwrap_or(&0);
let sort_score = *self.cache.get(&name).unwrap_or(&0);
let mut entry = MenuItem {
label: name.clone(),
@ -110,7 +118,7 @@ impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
working_dir: working_dir.clone(),
initial_sort_score: sort_score,
search_sort_score: 0.0,
data: Some(menu_item_data.clone()),
data: Some(self.data.clone()),
visible: true,
};
@ -127,7 +135,6 @@ impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
.or(icon.clone())
.unwrap_or("application-x-executable".to_string());
log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}");
entry.sub_elements.push(MenuItem {
label: action_name,
@ -137,7 +144,7 @@ impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
working_dir: working_dir.clone(),
initial_sort_score: 0,
search_sort_score: 0.0,
data: None,
data: Some(self.data.clone()),
visible: true,
});
}
@ -147,24 +154,28 @@ impl<T: Clone + std::marker::Send + std::marker::Sync> DRunProvider<T> {
})
.collect();
let mut seen_actions = HashSet::new();
let mut entries: Vec<MenuItem<T>> = entries
.into_iter()
.filter(|entry| seen_actions.insert(entry.action.clone()))
.collect();
log::info!(
"parsing desktop files took {}ms",
start.elapsed().as_millis()
);
gui::sort_menu_items_alphabetically_honor_initial_score(&mut entries);
DRunProvider {
items: entries,
cache_path,
cache: d_run_cache,
}
entries
}
}
impl<T: Clone> ItemProvider<T> for DRunProvider<T> {
impl<T: Clone + std::marker::Send + std::marker::Sync> ItemProvider<T> for DRunProvider<T> {
fn get_elements(&mut self, _: Option<&str>) -> Vec<MenuItem<T>> {
self.items.clone()
if self.items.is_none() {
self.items = Some(self.load().clone());
}
self.items.clone().unwrap()
}
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> Option<Vec<MenuItem<T>>> {

View file

@ -3,12 +3,12 @@ use std::env;
use anyhow::anyhow;
use worf_lib::config::Mode;
use worf_lib::{config, mode};
fn main() -> anyhow::Result<()> {
gtk4::init()?;
env_logger::Builder::new()
.parse_filters(&env::var("RUST_LOG").unwrap_or_else(|_| "error".to_owned()))
.format_timestamp_micros()
.init();
let args = config::parse_args();