Skip to content

Commit 66b67fa

Browse files
committed
perf: Arc-wrap workspace cache entries; macOS no longer assumed case-insensitive (PR #460)
1 parent 4225e5c commit 66b67fa

1 file changed

Lines changed: 52 additions & 34 deletions

File tree

crates/pet-hatch/src/lib.rs

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,28 @@ use serde::Deserialize;
5757
/// plugin's `PLUGIN_NAME` in Hatch's source.
5858
const VIRTUAL_ENV_SUBDIR: [&str; 2] = ["env", "virtual"];
5959

60-
/// Per-workspace cache of resolved Hatch virtual directories and the set
61-
/// of declared env names for that workspace. Each entry is
62-
/// `(workspace_root, resolved_virtual_dirs, env_matcher)` and is
63-
/// populated by `configure()`.
60+
/// Per-workspace cache entry: workspace root, resolved
61+
/// `dirs.env.virtual` paths, and the precomputed env-name allowlist.
6462
///
65-
/// `env_matcher` is used as a Hatch-specific guard when matching
66-
/// venvs in workspace-configured `dirs.env.virtual` directories: a shared
63+
/// `matcher` is used as a Hatch-specific guard when matching venvs in
64+
/// workspace-configured `dirs.env.virtual` directories: a shared
6765
/// directory like `~/.virtualenvs` can contain non-Hatch envs (created by
6866
/// virtualenvwrapper, plain `venv`, etc.), so we only claim a venv if its
6967
/// leaf directory name matches one of the env names declared in the
7068
/// project's Hatch configuration. The matcher pre-normalizes names so the
7169
/// `try_from()` hot path avoids per-call `to_lowercase()` / `format!()`
7270
/// allocations over the allowlist.
73-
type WorkspaceVirtualDirs = Vec<(PathBuf, Vec<PathBuf>, EnvNameMatcher)>;
71+
struct WorkspaceEntry {
72+
workspace: PathBuf,
73+
virtual_dirs: Vec<PathBuf>,
74+
matcher: EnvNameMatcher,
75+
}
76+
77+
/// Per-workspace cache populated by `configure()`. Entries are wrapped in
78+
/// `Arc` so `find()` can snapshot the cache (clone the Vec of Arcs) and
79+
/// release the lock cheaply before doing filesystem I/O — no deep
80+
/// `Vec<PathBuf>` / matcher clone per call.
81+
type WorkspaceVirtualDirs = Vec<Arc<WorkspaceEntry>>;
7482

7583
pub struct Hatch {
7684
/// Default storage directory for Hatch virtual environments — i.e.
@@ -132,11 +140,11 @@ impl Locator for Hatch {
132140
// — both `virtual_dirs` and `env_names` come from the same
133141
// TOML sections, so we read each file once here.
134142
let (virtual_dirs, env_names) = resolve_workspace_hatch_config(workspace);
135-
new_cache.push((
136-
workspace.clone(),
143+
new_cache.push(Arc::new(WorkspaceEntry {
144+
workspace: workspace.clone(),
137145
virtual_dirs,
138-
EnvNameMatcher::from_names(env_names),
139-
));
146+
matcher: EnvNameMatcher::from_names(env_names),
147+
}));
140148
}
141149
}
142150
*self
@@ -184,17 +192,17 @@ impl Locator for Hatch {
184192
.workspace_virtual_dirs
185193
.lock()
186194
.expect("workspace_virtual_dirs mutex poisoned");
187-
'workspaces: for (workspace, virtual_dirs, matcher) in cache.iter() {
188-
for virtual_dir in virtual_dirs {
195+
'workspaces: for entry in cache.iter() {
196+
for virtual_dir in &entry.virtual_dirs {
189197
if prefix_is_directly_under(&prefix, virtual_dir) {
190198
let env_name = prefix
191199
.file_name()
192200
.map(|n| n.to_string_lossy().to_string())
193201
.unwrap_or_default();
194-
if !matcher.matches(&env_name) {
202+
if !entry.matcher.matches(&env_name) {
195203
continue;
196204
}
197-
classification = Some((env_name, Some(workspace.clone())));
205+
classification = Some((env_name, Some(entry.workspace.clone())));
198206
break 'workspaces;
199207
}
200208
}
@@ -238,16 +246,22 @@ impl Locator for Hatch {
238246
}
239247

240248
// 2. Walk project-local virtual directories for each configured workspace.
241-
// Apply the same env-name guard as `try_from()` so shared directories
242-
// (e.g. `~/.virtualenvs`) only yield the workspace's declared envs.
243-
let workspaces = self
249+
// Snapshot the cache (cheap `Arc` clones) under the lock, then
250+
// release the lock before doing filesystem I/O. Apply the same
251+
// env-name guard as `try_from()` so shared directories (e.g.
252+
// `~/.virtualenvs`) only yield the workspace's declared envs.
253+
let workspaces: Vec<Arc<WorkspaceEntry>> = self
244254
.workspace_virtual_dirs
245255
.lock()
246256
.expect("workspace_virtual_dirs mutex poisoned")
247257
.clone();
248-
for (workspace, virtual_dirs, matcher) in &workspaces {
249-
for virtual_dir in virtual_dirs {
250-
for env in find_envs_in_flat_dir(virtual_dir, Some(workspace.clone()), matcher) {
258+
for entry in &workspaces {
259+
for virtual_dir in &entry.virtual_dirs {
260+
for env in find_envs_in_flat_dir(
261+
virtual_dir,
262+
Some(entry.workspace.clone()),
263+
&entry.matcher,
264+
) {
251265
reporter.report_environment(&env);
252266
}
253267
}
@@ -616,21 +630,25 @@ fn find_envs_in_default_storage(storage: &Path) -> Vec<PythonEnvironment> {
616630
/// (`try_from()` / `find_envs_in_flat_dir()`) avoids per-call `format!()`
617631
/// allocations.
618632
///
619-
/// On case-insensitive filesystems (Windows / default macOS) the on-disk
620-
/// leaf may differ in case from the TOML key, so we lowercase both sides
621-
/// on those platforms at construction time.
633+
/// On case-insensitive filesystems (default on Windows) the on-disk leaf
634+
/// may differ in case from the TOML key, so we lowercase both sides on
635+
/// Windows at construction time. macOS volumes can be either case-sensitive
636+
/// (default APFS) or case-insensitive (HFS+ / case-insensitive APFS), and
637+
/// `norm_case()` itself does not case-fold on macOS — so we keep the
638+
/// allowlist comparison byte-exact there to stay consistent with how paths
639+
/// are normalized elsewhere in this crate.
622640
#[derive(Clone, Default, Debug)]
623641
struct EnvNameMatcher {
624642
/// (normalized_name, normalized_name + ".") pairs.
625643
entries: Vec<(String, String)>,
626644
}
627645

628646
fn normalize_env_name(s: &str) -> String {
629-
#[cfg(any(windows, target_os = "macos"))]
647+
#[cfg(windows)]
630648
{
631649
s.to_lowercase()
632650
}
633-
#[cfg(not(any(windows, target_os = "macos")))]
651+
#[cfg(not(windows))]
634652
{
635653
s.to_string()
636654
}
@@ -802,11 +820,11 @@ mod tests {
802820
let env_names = resolve_project_env_names(workspace);
803821
Hatch {
804822
default_virtual_dir,
805-
workspace_virtual_dirs: Arc::new(Mutex::new(vec![(
806-
workspace.to_path_buf(),
823+
workspace_virtual_dirs: Arc::new(Mutex::new(vec![Arc::new(WorkspaceEntry {
824+
workspace: workspace.to_path_buf(),
807825
virtual_dirs,
808-
EnvNameMatcher::from_names(env_names),
809-
)])),
826+
matcher: EnvNameMatcher::from_names(env_names),
827+
})])),
810828
}
811829
}
812830

@@ -1126,8 +1144,8 @@ mod tests {
11261144

11271145
let cached = locator.workspace_virtual_dirs.lock().unwrap().clone();
11281146
assert_eq!(cached.len(), 1);
1129-
assert_eq!(cached[0].0, project);
1130-
assert_eq!(cached[0].1, vec![norm_case(&virtual_dir)]);
1147+
assert_eq!(cached[0].workspace, project);
1148+
assert_eq!(cached[0].virtual_dirs, vec![norm_case(&virtual_dir)]);
11311149
}
11321150

11331151
#[cfg(target_os = "linux")]
@@ -1424,9 +1442,9 @@ mod tests {
14241442
assert_eq!(envs.len(), 2);
14251443
}
14261444

1427-
#[cfg(any(windows, target_os = "macos"))]
1445+
#[cfg(windows)]
14281446
#[test]
1429-
fn env_name_matches_is_case_insensitive_on_case_folding_filesystems() {
1447+
fn env_name_matches_is_case_insensitive_on_windows() {
14301448
let mut names = HashSet::new();
14311449
names.insert("Default".to_string());
14321450
let matcher = EnvNameMatcher::from_names(names);

0 commit comments

Comments
 (0)