perf(hash): swap std HashMap/HashSet for rustc-hash FxHash

Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.

Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-05 00:35:53 +03:00
commit 70702d063f
30 changed files with 102 additions and 98 deletions

View file

@ -15,7 +15,7 @@
// a visited set so a self-referential block produces a marker rather than
// recursing forever.
use std::collections::HashMap;
use rustc_hash::FxHashMap as HashMap;
use std::sync::Arc;
use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3};
@ -206,8 +206,8 @@ impl BlockCache {
/// Walk all entities + all block_record contents collecting every distinct
/// `block_name` that appears in an Insert (transitively).
fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
use std::collections::HashSet;
let mut seen: HashSet<String> = HashSet::new();
use rustc_hash::FxHashSet as HashSet;
let mut seen: HashSet<String> = HashSet::default();
let mut queue: Vec<String> = Vec::new();
for entity in doc.entities() {

View file

@ -4,7 +4,7 @@
//! to 2-D pixel coordinates, then compared against the cursor or selection box.
//! This matches the visual result the user sees.
use std::collections::HashMap;
use rustc_hash::FxHashMap as HashMap;
use acadrust::Handle;
use glam::{Mat4, Vec3};

View file

@ -16,7 +16,7 @@
// Cap height is 9 glyph units, matching the `height / 9.0` text scale used
// throughout the renderer.
use std::collections::{HashMap, HashSet};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::sync::{Mutex, OnceLock};
// ── Embedded fonts ─────────────────────────────────────────────────────────
@ -154,7 +154,7 @@ fn warn_missing_glyph(font_name: &str, ch: char) {
if ch.is_ascii() {
return;
}
let set = WARNED_GLYPHS.get_or_init(|| Mutex::new(HashSet::new()));
let set = WARNED_GLYPHS.get_or_init(|| Mutex::new(HashSet::default()));
if let Ok(mut guard) = set.lock() {
if guard.insert((font_name.to_string(), ch)) {
eprintln!(
@ -167,7 +167,7 @@ fn warn_missing_glyph(font_name: &str, ch: char) {
fn fonts_map() -> &'static HashMap<String, Font> {
FONTS.get_or_init(|| {
let mut map = HashMap::new();
let mut map = HashMap::default();
// Register every font under its stem and its `# Name:` header.
for (stem, src) in FONTS_SRC {
let f = parse_lff(src);
@ -461,12 +461,12 @@ fn parse_lff(src: &str) -> Font {
letter_spacing: 3.0,
word_spacing: 6.75,
line_spacing: 1.0,
glyphs: HashMap::new(),
shapes: HashMap::new(),
glyphs: HashMap::default(),
shapes: HashMap::default(),
};
let mut raw: HashMap<char, RawGlyph> = HashMap::new();
let mut raw_shapes: HashMap<String, RawGlyph> = HashMap::new();
let mut raw: HashMap<char, RawGlyph> = HashMap::default();
let mut raw_shapes: HashMap<String, RawGlyph> = HashMap::default();
let mut cur: Option<char> = None;
let mut cur_name: Option<String> = None;
let mut cur_glyph = RawGlyph::default();

View file

@ -62,7 +62,7 @@ use truck_modeling::{
use iced::time::Duration;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
@ -256,9 +256,9 @@ struct OffsetPrep {
/// `Some` when the model BlockRecord enumerates its entities; the offset
/// scan uses this set directly. `None` falls back to the legacy
/// permissive owner-based interpretation.
mspace_set: Option<std::collections::HashSet<Handle>>,
mspace_set: Option<rustc_hash::FxHashSet<Handle>>,
any_enumerated: bool,
owned_by_other_block: std::collections::HashSet<Handle>,
owned_by_other_block: rustc_hash::FxHashSet<Handle>,
}
fn offset_prep(doc: &acadrust::CadDocument, model_block: Handle) -> OffsetPrep {
@ -266,21 +266,21 @@ fn offset_prep(doc: &acadrust::CadDocument, model_block: Handle) -> OffsetPrep {
.block_records
.iter()
.find(|br| br.handle == model_block);
let mspace_set: Option<std::collections::HashSet<Handle>> = model_br
let mspace_set: Option<rustc_hash::FxHashSet<Handle>> = model_br
.filter(|br| !br.entity_handles.is_empty())
.map(|br| br.entity_handles.iter().copied().collect());
let any_enumerated = doc
.block_records
.iter()
.any(|br| !br.entity_handles.is_empty());
let owned_by_other_block: std::collections::HashSet<Handle> = if mspace_set.is_none() {
let owned_by_other_block: rustc_hash::FxHashSet<Handle> = if mspace_set.is_none() {
doc.block_records
.iter()
.filter(|br| br.handle != model_block)
.flat_map(|br| br.entity_handles.iter().copied())
.collect()
} else {
std::collections::HashSet::new()
rustc_hash::FxHashSet::default()
};
OffsetPrep { mspace_set, any_enumerated, owned_by_other_block }
}
@ -718,31 +718,31 @@ impl Scene {
active_model_tile: std::cell::Cell::new(0),
selection: Rc::new(RefCell::new(SelectionState::default())),
document: CadDocument::new(),
selected: HashSet::new(),
hidden: HashSet::new(),
selected: HashSet::default(),
hidden: HashSet::default(),
hover_highlight: None,
transparency_display: true,
selection_filter: HashSet::new(),
selection_filter: HashSet::default(),
preview_wires: vec![],
interim_wire: None,
camera_generation: 0,
geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed),
wire_cache: RefCell::new(None),
model_tile_wire_cache: RefCell::new(HashMap::new()),
model_tile_wire_cache: RefCell::new(HashMap::default()),
sort_cache: RefCell::new(None),
draw_depth_cache: RefCell::new(None),
hatch_cache: RefCell::new(None),
wipeout_cache: RefCell::new(None),
image_cache: RefCell::new(None),
mesh_cache: RefCell::new(None),
viewport_wire_cache: RefCell::new(HashMap::new()),
viewport_wire_cache: RefCell::new(HashMap::default()),
paper_sheet_cache: RefCell::new(None),
paper_projected_cache: RefCell::new(HashMap::new()),
paper_projected_cache: RefCell::new(HashMap::default()),
current_layout: "Model".to_string(),
hatches: HashMap::new(),
meshes: HashMap::new(),
model_solids: HashMap::new(),
images: HashMap::new(),
hatches: HashMap::default(),
meshes: HashMap::default(),
model_solids: HashMap::default(),
images: HashMap::default(),
active_viewport: None,
bg_color: [0.11, 0.11, 0.11, 1.0],
paper_bg_color: [1.0, 1.0, 1.0, 1.0],
@ -1455,7 +1455,7 @@ impl Scene {
// Deduplicate by name: prefer the entry with a non-null block_record (the
// real layout from the file) over the default placeholder created by
// CadDocument::new().
let mut by_name: std::collections::HashMap<String, (i16, Handle)> = Default::default();
let mut by_name: rustc_hash::FxHashMap<String, (i16, Handle)> = Default::default();
for obj in self.document.objects.values() {
if let ObjectType::Layout(l) = obj {
if l.name == "Model" || l.name.is_empty() {
@ -1628,7 +1628,7 @@ impl Scene {
}
use acadrust::objects::ObjectType;
// Per-block SortEntitiesTable overrides: block -> (entity_val -> sort_val).
let mut overrides: HashMap<Handle, HashMap<u64, u64>> = HashMap::new();
let mut overrides: HashMap<Handle, HashMap<u64, u64>> = HashMap::default();
for obj in self.document.objects.values() {
if let ObjectType::SortEntitiesTable(t) = obj {
if !t.is_empty() {
@ -1643,7 +1643,7 @@ impl Scene {
}
let ms = self.model_space_block_handle();
// Group entities by owning block, carrying each entity's effective key.
let mut by_block: HashMap<Handle, Vec<(u64, u64)>> = HashMap::new();
let mut by_block: HashMap<Handle, Vec<(u64, u64)>> = HashMap::default();
for e in self.document.entities() {
let c = e.common();
// 3D meshes keep real geometric depth — exclude them from
@ -1667,7 +1667,7 @@ impl Scene {
.unwrap_or(hv);
by_block.entry(block).or_default().push((hv, eff));
}
let mut depth_map: HashMap<u64, f32> = HashMap::new();
let mut depth_map: HashMap<u64, f32> = HashMap::default();
for (_block, mut v) in by_block {
v.sort_by_key(|(_, eff)| *eff);
let denom = (v.len() as f32) + 1.0;
@ -1928,7 +1928,7 @@ impl Scene {
.unwrap_or(true);
if needs_rebuild {
let mut idx: HashMap<Handle, HashMap<u64, u64>> = HashMap::new();
let mut idx: HashMap<Handle, HashMap<u64, u64>> = HashMap::default();
for obj in self.document.objects.values() {
if let ObjectType::SortEntitiesTable(t) = obj {
if !t.is_empty() {
@ -2187,7 +2187,7 @@ impl Scene {
}
}
}
let mut map: HashMap<Handle, Handle> = HashMap::new();
let mut map: HashMap<Handle, Handle> = HashMap::default();
for br in self.document.block_records.iter() {
for &eh in &br.entity_handles {
map.insert(eh, br.handle);
@ -4098,11 +4098,11 @@ impl Scene {
pub fn clear(&mut self) {
self.document = CadDocument::new();
self.selected = HashSet::new();
self.selected = HashSet::default();
self.preview_wires = vec![];
self.current_layout = "Model".to_string();
self.hatches = HashMap::new();
self.meshes = HashMap::new();
self.hatches = HashMap::default();
self.meshes = HashMap::default();
*self.camera.borrow_mut() = Camera::default();
self.camera_generation += 1;
self.bump_geometry();
@ -4201,7 +4201,7 @@ impl Scene {
if self.selected.is_empty() {
return 0;
}
let pairs: std::collections::HashSet<(&'static str, String)> = self
let pairs: rustc_hash::FxHashSet<(&'static str, String)> = self
.selected
.iter()
.filter_map(|h| self.document.get_entity(*h))
@ -6276,7 +6276,7 @@ impl Scene {
vp_handle: Handle,
screen_height_px: f32,
) -> Vec<WireModel> {
use std::collections::HashSet as HSet;
use rustc_hash::FxHashSet as HSet;
let (frozen, vp_anno_scale, vp_aspect) = match self.document.get_entity(vp_handle) {
Some(EntityType::Viewport(vp)) => {
@ -6295,7 +6295,7 @@ impl Scene {
};
(f, anno, aspect)
}
_ => (HSet::new(), 1.0_f32, 1.0_f32),
_ => (HSet::default(), 1.0_f32, 1.0_f32),
};
// Drive the per-viewport view_aabb / wpp from the *effective* camera

View file

@ -93,7 +93,7 @@ impl Face3DGpu {
face3d_wires: &[WireModel],
all_wires: &[WireModel],
keep_3d_mesh_fills: bool,
depth_map: &std::collections::HashMap<u64, f32>,
depth_map: &rustc_hash::FxHashMap<u64, f32>,
) -> Self {
let depth_of = |w: &WireModel| -> f32 {
w.name

View file

@ -891,7 +891,7 @@ impl Pipeline {
&mut self,
device: &wgpu::Device,
wires: &[WireModel],
depth_map: &std::collections::HashMap<u64, f32>,
depth_map: &rustc_hash::FxHashMap<u64, f32>,
) {
let depth_of = |w: &WireModel| -> f32 {
w.name
@ -987,7 +987,7 @@ impl Pipeline {
face3d_wires: &[WireModel],
all_wires: &[WireModel],
wireframe_only: bool,
depth_map: &std::collections::HashMap<u64, f32>,
depth_map: &rustc_hash::FxHashMap<u64, f32>,
) {
// Edge buffer is always built from `face3d_wires`, so 3DFACE
// outlines stay on the screen regardless of mode.

View file

@ -267,7 +267,7 @@ fn emit_wire_instances(wire: &WireModel, color: [f32; 4], draw_depth: f32) -> Ve
/// Looks up a wire's draw-order depth from the per-entity map using the
/// handle encoded in its `name`. Falls back to 0.0 (transient / preview
/// wires that carry no document handle).
fn wire_draw_depth(wire: &WireModel, depth_map: &std::collections::HashMap<u64, f32>) -> f32 {
fn wire_draw_depth(wire: &WireModel, depth_map: &rustc_hash::FxHashMap<u64, f32>) -> f32 {
wire
.name
.parse::<u64>()
@ -290,7 +290,7 @@ impl WireGpu {
pub fn from_batch(
device: &wgpu::Device,
wires: &[WireModel],
depth_map: &std::collections::HashMap<u64, f32>,
depth_map: &rustc_hash::FxHashMap<u64, f32>,
) -> Vec<Self> {
let total_segs: usize = wires.iter().map(|w| w.points.len().saturating_sub(1)).sum();
if total_segs == 0 {

View file

@ -57,7 +57,7 @@ pub struct QuadTree {
nodes: Vec<Node>,
/// `handle → (node_idx, item_idx_within_node)` so removal/update
/// is O(1) without walking the tree.
locator: std::collections::HashMap<Handle, (u32, u32)>,
locator: rustc_hash::FxHashMap<Handle, (u32, u32)>,
/// Items whose AABB falls outside the root bounds. Surfaced on
/// every query — small set in practice (typically empty for
/// well-bounded drawings).
@ -74,7 +74,7 @@ impl QuadTree {
};
Self {
nodes: vec![root],
locator: std::collections::HashMap::new(),
locator: rustc_hash::FxHashMap::default(),
overflow: Vec::new(),
}
}

View file

@ -39,7 +39,7 @@ pub struct ViewportData {
/// by the wire / face3d pipelines as a clip-z bias. WireModels carry no
/// depth field (84 construction sites); the bias is looked up by handle
/// at GPU-upload time from this map instead.
pub(super) draw_depths: Arc<std::collections::HashMap<u64, f32>>,
pub(super) draw_depths: Arc<rustc_hash::FxHashMap<u64, f32>>,
pub(super) hatches: Arc<Vec<HatchModel>>,
/// Wipeout fills — rendered in a separate pass AFTER wires.
pub(super) wipeout_hatches: Arc<Vec<HatchModel>>,

View file

@ -10,7 +10,7 @@
// All other surface types are silently skipped; partial results are still
// returned so the solid renders with at least its planar faces.
use std::collections::HashSet;
use rustc_hash::FxHashSet as HashSet;
use std::f64::consts::TAU;
use acadrust::entities::acis::types::Sense;
@ -237,7 +237,7 @@ fn collect_face_polygon(sat: &SatDocument, face: &SatFace) -> Vec<[f64; 3]> {
let first_ptr = sat_loop.first_coedge();
let mut cur = first_ptr;
let mut pts: Vec<[f64; 3]> = Vec::new();
let mut visited: HashSet<i32> = HashSet::new();
let mut visited: HashSet<i32> = HashSet::default();
loop {
if cur.is_null() {