perf: use rustc-hash for HashMap and HashSet

Since we already depend on `rustc-hash` transiently, this doesn't add
any more dependencies. As long as DOS attacks aren't a concern (which I
don't think they are?), this should be free performance.

In my (admittedly naive) testing, this really improved CPU usage in some
cases, which is pretty nice to get for free.
This commit is contained in:
Cheong Lau 2025-10-19 08:46:12 +10:00
parent 4be92ae8ca
commit 43a9fca4ec
11 changed files with 68 additions and 68 deletions

1
Cargo.lock generated
View file

@ -1496,6 +1496,7 @@ dependencies = [
"recently-used-xbel",
"regex",
"rust-embed",
"rustc-hash 2.1.1",
"serde",
"shlex",
"slotmap",

View file

@ -29,6 +29,7 @@ notify-rust = { version = "4", optional = true }
open = "5.3.2"
paste = "1.0"
regex = "1"
rustc-hash = "2.1"
serde = { version = "1", features = ["serde_derive"] }
shlex = { version = "1.3" }
tempfile = "3"

View file

@ -47,10 +47,11 @@ use notify_debouncer_full::{
DebouncedEvent, Debouncer, RecommendedCache, new_debouncer,
notify::{self, RecommendedWatcher},
};
use rustc_hash::{FxHashMap, FxHashSet};
use slotmap::Key as SlotMapKey;
use std::{
any::TypeId,
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
collections::{BTreeMap, BTreeSet, HashMap, VecDeque},
env, fmt, fs,
future::Future,
io,
@ -67,6 +68,7 @@ use trash::TrashItem;
use wayland_client::{Proxy, protocol::wl_output::WlOutput};
use crate::{
FxOrderMap,
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
config::{
AppTheme, Config, DesktopConfig, Favorite, IconSizes, TIME_CONFIG_ID, TabConfig,
@ -654,16 +656,16 @@ pub struct App {
dialog_pages: DialogPages,
dialog_text_input: widget::Id,
key_binds: HashMap<KeyBind, Action>,
margin: HashMap<window::Id, (f32, f32, f32, f32)>,
margin: FxHashMap<window::Id, (f32, f32, f32, f32)>,
mime_app_cache: MimeAppCache,
modifiers: Modifiers,
mounter_items: HashMap<MounterKey, MounterItems>,
mounter_items: FxHashMap<MounterKey, MounterItems>,
must_save_sort_names: bool,
network_drive_connecting: Option<(MounterKey, String)>,
network_drive_input: String,
#[cfg(feature = "notify")]
notification_opt: Option<Arc<Mutex<notify_rust::NotificationHandle>>>,
overlap: HashMap<String, (window::Id, Rectangle)>,
overlap: FxHashMap<String, (window::Id, Rectangle)>,
pending_operation_id: u64,
pending_operations: BTreeMap<u64, (Operation, Controller)>,
progress_operations: BTreeSet<u64>,
@ -673,17 +675,17 @@ pub struct App {
search_id: widget::Id,
size: Option<Size>,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
layer_sizes: HashMap<window::Id, Size>,
layer_sizes: FxHashMap<window::Id, Size>,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
surface_ids: HashMap<WlOutput, WindowId>,
surface_ids: FxHashMap<WlOutput, WindowId>,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
surface_names: HashMap<WindowId, String>,
surface_names: FxHashMap<WindowId, String>,
toasts: widget::toaster::Toasts<Message>,
watcher_opt: Option<(
Debouncer<RecommendedWatcher, RecommendedCache>,
HashSet<PathBuf>,
FxHashSet<PathBuf>,
)>,
windows: HashMap<window::Id, WindowKind>,
windows: FxHashMap<window::Id, WindowKind>,
nav_dnd_hover: Option<(Location, Instant)>,
tab_dnd_hover: Option<(Entity, Instant)>,
nav_drag_id: DragId,
@ -699,7 +701,7 @@ impl App {
// Associate all paths to its MIME type
// This allows handling paths as groups if possible, such as launching a single video
// player that is passed every path.
let mut groups: HashMap<Mime, Vec<PathBuf>> = HashMap::new();
let mut groups: FxHashMap<Mime, Vec<PathBuf>> = FxHashMap::default();
let mut all_archives = true;
let supported_archive_types = crate::archive::SUPPORTED_ARCHIVE_TYPES
.iter()
@ -915,7 +917,7 @@ impl App {
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
fn handle_overlap(&mut self) {
let mut overlaps: HashMap<_, _> = self
let mut overlaps: FxHashMap<_, _> = self
.windows
.keys()
.map(|k| (*k, (0., 0., 0., 0.)))
@ -1573,7 +1575,7 @@ impl App {
fn update_watcher(&mut self) -> Task<Message> {
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
let mut new_paths = HashSet::new();
let mut new_paths = FxHashSet::default();
for entity in self.tab_model.iter() {
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
if let Some(path) = tab.location.path_opt() {
@ -1942,7 +1944,7 @@ impl App {
fn get_apps_for_mime(&self, mime_type: &Mime) -> Vec<(&MimeApp, MimeAppMatch)> {
let mut results = Vec::new();
let mut dedupe = HashSet::new();
let mut dedupe = FxHashSet::default();
// start with exact matches
for mime_app in self.mime_app_cache.get(mime_type) {
@ -2124,16 +2126,16 @@ impl Application for App {
dialog_pages: DialogPages::new(),
dialog_text_input: widget::Id::unique(),
key_binds,
margin: HashMap::new(),
margin: FxHashMap::default(),
mime_app_cache: MimeAppCache::new(),
modifiers: Modifiers::empty(),
mounter_items: HashMap::new(),
mounter_items: FxHashMap::default(),
must_save_sort_names: false,
network_drive_connecting: None,
network_drive_input: String::new(),
#[cfg(feature = "notify")]
notification_opt: None,
overlap: HashMap::new(),
overlap: FxHashMap::default(),
pending_operation_id: 0,
pending_operations: BTreeMap::new(),
progress_operations: BTreeSet::new(),
@ -2143,12 +2145,12 @@ impl Application for App {
search_id: widget::Id::unique(),
size: None,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
surface_ids: HashMap::new(),
surface_ids: FxHashMap::default(),
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
surface_names: HashMap::new(),
surface_names: FxHashMap::default(),
toasts: widget::toaster::Toasts::new(Message::CloseToast),
watcher_opt: None,
windows: HashMap::new(),
windows: FxHashMap::default(),
nav_dnd_hover: None,
tab_dnd_hover: None,
nav_drag_id: DragId::new(),
@ -2156,7 +2158,7 @@ impl Application for App {
auto_scroll_speed: None,
file_dialog_opt: None,
#[cfg(all(feature = "wayland", feature = "desktop-applet"))]
layer_sizes: HashMap::new(),
layer_sizes: FxHashMap::default(),
};
let mut commands = vec![app.update_config()];
@ -3211,7 +3213,7 @@ impl Application for App {
Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
{
Some(watcher) => {
self.watcher_opt = Some((watcher, HashSet::new()));
self.watcher_opt = Some((watcher, FxHashSet::default()));
return self.update_watcher();
}
None => {
@ -4678,8 +4680,8 @@ impl Application for App {
Message::SaveSortNames => {
self.must_save_sort_names = false;
if let Some(state_handler) = self.state_handler.as_ref() {
if let Err(err) =
state_handler.set::<ordermap::OrderMap<String, (HeadingOptions, bool)>>(
if let Err(err) = state_handler
.set::<FxOrderMap<String, (HeadingOptions, bool)>>(
"sort_names",
self.state.sort_names.clone(),
)

View file

@ -8,10 +8,10 @@ use cosmic::{
iced::Subscription,
theme,
};
use ordermap::OrderMap;
use serde::{Deserialize, Serialize};
use crate::{
FxOrderMap,
app::App,
tab::{HeadingOptions, Location, View},
};
@ -115,13 +115,13 @@ pub enum TypeToSearch {
#[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct State {
pub sort_names: ordermap::OrderMap<String, (HeadingOptions, bool)>,
pub sort_names: FxOrderMap<String, (HeadingOptions, bool)>,
}
impl Default for State {
fn default() -> Self {
Self {
sort_names: OrderMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
sort_names: FxOrderMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
(
Location::Path(dir).normalize().to_string(),
(HeadingOptions::Modified, false),

View file

@ -25,9 +25,10 @@ use notify_debouncer_full::{
notify::{self, RecommendedWatcher},
};
use recently_used_xbel::update_recently_used;
use rustc_hash::{FxHashMap, FxHashSet};
use std::{
any::TypeId,
collections::{HashMap, HashSet, VecDeque},
collections::{HashMap, VecDeque},
env, fmt, fs,
num::NonZeroU16,
path::PathBuf,
@ -509,7 +510,7 @@ struct App {
filter_selected: Option<usize>,
filename_id: widget::Id,
modifiers: Modifiers,
mounter_items: HashMap<MounterKey, MounterItems>,
mounter_items: FxHashMap<MounterKey, MounterItems>,
nav_model: segmented_button::SingleSelectModel,
result_opt: Option<DialogResult>,
search_id: widget::Id,
@ -517,7 +518,7 @@ struct App {
key_binds: HashMap<KeyBind, Action>,
watcher_opt: Option<(
Debouncer<RecommendedWatcher, RecommendedCache>,
HashSet<PathBuf>,
FxHashSet<PathBuf>,
)>,
auto_scroll_speed: Option<i16>,
}
@ -871,7 +872,7 @@ impl App {
fn update_watcher(&mut self) -> Task<Message> {
if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
let mut new_paths = HashSet::new();
let mut new_paths = FxHashSet::default();
if let Some(path) = &self.tab.location.path_opt() {
new_paths.insert(path.to_path_buf());
}
@ -981,7 +982,7 @@ impl Application for App {
filter_selected: None,
filename_id: widget::Id::unique(),
modifiers: Modifiers::empty(),
mounter_items: HashMap::new(),
mounter_items: FxHashMap::default(),
nav_model: segmented_button::ModelBuilder::default().build(),
result_opt: None,
search_id: widget::Id::unique(),
@ -1519,7 +1520,7 @@ impl Application for App {
Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
{
Some(watcher) => {
self.watcher_opt = Some((watcher, HashSet::new()));
self.watcher_opt = Some((watcher, FxHashSet::default()));
return self.update_watcher();
}
None => {

View file

@ -27,6 +27,8 @@ pub mod tab;
mod thumbnail_cacher;
mod thumbnailer;
pub(crate) type FxOrderMap<K, V> = ordermap::OrderMap<K, V, rustc_hash::FxBuildHasher>;
pub(crate) fn err_str<T: ToString>(err: T) -> String {
err.to_string()
}

View file

@ -5,9 +5,9 @@
use cosmic::desktop;
use cosmic::widget;
pub use mime_guess::Mime;
use rustc_hash::FxHashMap;
use std::{
cmp::Ordering,
collections::HashMap,
env,
ffi::OsStr,
fs, io,
@ -221,8 +221,8 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
pub struct MimeAppCache {
apps: Vec<MimeApp>,
cache: HashMap<Mime, Vec<MimeApp>>,
icons: HashMap<Mime, Vec<widget::icon::Handle>>,
cache: FxHashMap<Mime, Vec<MimeApp>>,
icons: FxHashMap<Mime, Vec<widget::icon::Handle>>,
terminals: Vec<MimeApp>,
}
@ -230,8 +230,8 @@ impl MimeAppCache {
pub fn new() -> Self {
let mut mime_app_cache = Self {
apps: Vec::new(),
cache: HashMap::new(),
icons: HashMap::new(),
cache: FxHashMap::default(),
icons: FxHashMap::default(),
terminals: Vec::new(),
};
mime_app_cache.reload();

View file

@ -2,8 +2,8 @@
use cosmic::widget::icon;
use mime_guess::Mime;
use rustc_hash::FxHashMap;
use std::{
collections::HashMap,
fs,
path::Path,
sync::{LazyLock, Mutex},
@ -18,14 +18,14 @@ struct MimeIconKey {
}
struct MimeIconCache {
cache: HashMap<MimeIconKey, Option<icon::Handle>>,
cache: FxHashMap<MimeIconKey, Option<icon::Handle>>,
shared_mime_info: xdg_mime::SharedMimeInfo,
}
impl MimeIconCache {
pub fn new() -> Self {
Self {
cache: HashMap::new(),
cache: FxHashMap::default(),
shared_mime_info: xdg_mime::SharedMimeInfo::new(),
}
}

View file

@ -49,7 +49,7 @@ use icu::{
use image::ImageDecoder;
use jxl_oxide::integration::JxlDecoder;
use mime_guess::{Mime, mime};
use ordermap::OrderMap;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
@ -73,6 +73,7 @@ use trash::TrashItemSize;
use walkdir::WalkDir;
use crate::{
FxOrderMap,
app::{Action, PreviewItem, PreviewKind},
clipboard::{ClipboardCopy, ClipboardKind, ClipboardPaste},
config::{DesktopConfig, ICON_SCALE_MAX, ICON_SIZE_GRID, IconSizes, TabConfig, ThumbCfg},
@ -100,9 +101,9 @@ const THUMBNAIL_SIZE: u32 = (ICON_SIZE_GRID as u32) * (ICON_SCALE_MAX as u32);
pub static THUMB_SEMAPHORE: LazyLock<tokio::sync::Semaphore> =
LazyLock::new(|| tokio::sync::Semaphore::const_new(num_cpus::get()));
pub(crate) static SORT_OPTION_FALLBACK: LazyLock<HashMap<String, (HeadingOptions, bool)>> =
pub(crate) static SORT_OPTION_FALLBACK: LazyLock<FxHashMap<String, (HeadingOptions, bool)>> =
LazyLock::new(|| {
HashMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
FxHashMap::from_iter(dirs::download_dir().into_iter().map(|dir| {
(
Location::Path(dir).normalize().to_string(),
(HeadingOptions::Modified, false),
@ -131,8 +132,8 @@ static MODE_NAMES: LazyLock<Vec<String>> = LazyLock::new(|| {
]
});
static SPECIAL_DIRS: LazyLock<HashMap<PathBuf, &'static str>> = LazyLock::new(|| {
let mut special_dirs = HashMap::new();
static SPECIAL_DIRS: LazyLock<FxHashMap<PathBuf, &'static str>> = LazyLock::new(|| {
let mut special_dirs = FxHashMap::default();
if let Some(dir) = dirs::document_dir() {
special_dirs.insert(dir, "folder-documents");
}
@ -534,25 +535,17 @@ pub enum FsKind {
pub fn fs_kind(metadata: &Metadata) -> FsKind {
//TODO: method to reload remote filesystems dynamically
//TODO: fix for https://github.com/eminence/procfs/issues/262
static DEVICES: LazyLock<HashMap<u64, FsKind>> = LazyLock::new(|| {
let mut devices = HashMap::new();
static DEVICES: LazyLock<FxHashMap<u64, FsKind>> = LazyLock::new(|| {
let mut devices = FxHashMap::default();
match procfs::process::Process::myself() {
Ok(process) => match process.mountinfo() {
Ok(mount_infos) => {
for mount_info in mount_infos.iter() {
devices = FxHashMap::from_iter(mount_infos.iter().filter_map(|mount_info| {
let mut parts = mount_info.majmin.split(':');
let Some(major_str) = parts.next() else {
continue;
};
let Some(minor_str) = parts.next() else {
continue;
};
let Ok(major) = major_str.parse::<libc::c_uint>() else {
continue;
};
let Ok(minor) = minor_str.parse::<libc::c_uint>() else {
continue;
};
let major_str = parts.next()?;
let minor_str = parts.next()?;
let major = major_str.parse::<libc::c_uint>().ok()?;
let minor = minor_str.parse::<libc::c_uint>().ok()?;
let dev = libc::makedev(major, minor);
//TODO: make sure this list is exhaustive
let kind = match mount_info.fs_type.as_str() {
@ -561,8 +554,8 @@ pub fn fs_kind(metadata: &Metadata) -> FsKind {
"fuse.gvfsd-fuse" => FsKind::Gvfs,
_ => FsKind::Local,
};
devices.insert(dev, kind);
}
Some((dev, kind))
}));
}
Err(err) => {
log::warn!("failed to get mount info: {err}");
@ -2525,7 +2518,7 @@ impl Tab {
location: Location,
config: TabConfig,
thumb_config: ThumbCfg,
sorting_options: Option<&OrderMap<String, (HeadingOptions, bool)>>,
sorting_options: Option<&FxOrderMap<String, (HeadingOptions, bool)>>,
scrollable_id: widget::Id,
window_id: Option<window::Id>,
) -> Self {

View file

@ -1,7 +1,7 @@
use image::DynamicImage;
use md5::{Digest, Md5};
use rustc_hash::FxHashMap;
use std::{
collections::HashMap,
error::Error,
fs::{self, File},
io::{self, BufReader, BufWriter},
@ -143,7 +143,7 @@ impl ThumbnailCacher {
let mut reader = decoder.read_info()?;
let (width, height, color_type, bit_depth, mut text_chunks) = {
let info = reader.info();
let text_chunks: HashMap<String, String> = info
let text_chunks: FxHashMap<String, String> = info
.uncompressed_latin1_text
.clone()
.into_iter()

View file

@ -2,8 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use mime_guess::Mime;
use rustc_hash::FxHashMap;
use std::{
collections::HashMap,
fs,
path::Path,
process,
@ -56,13 +56,13 @@ impl Thumbnailer {
}
pub struct ThumbnailerCache {
cache: HashMap<Mime, Vec<Thumbnailer>>,
cache: FxHashMap<Mime, Vec<Thumbnailer>>,
}
impl ThumbnailerCache {
pub fn new() -> Self {
let mut thumbnailer_cache = Self {
cache: HashMap::new(),
cache: FxHashMap::default(),
};
thumbnailer_cache.reload();
thumbnailer_cache