Skip to content

Commit d639fdf

Browse files
committed
cache validated FLASHDeconv exe paths to skip per-scan syscalls
Each Deconvoluter.Deconvolute(scan, params) call constructs a fresh RealFLASHDeconvolutionAlgorithm and runs ResolveExePath, which hits File.Exists on the explicit path -- or walks the well-known list and PATH on misses. On a per-scan loop that's redundant filesystem syscalls per minute; the exe doesn't move between calls. - New public FlashDeconvExePathRegistry static class (ConcurrentDictionary of validated paths) with a Register(path) entry point. Production callers (e.g. MetaMorpheus from GlobalSettings.FLASHDeconvExecutablePath) can prime the registry once at startup so every Deconvolute call becomes lookup-only. Ad-hoc callers don't have to do anything -- ResolveExePath caches lazily on first miss either way. - ResolveExePath now consults the registry first; on miss, runs the existing validate-and-resolve and caches the result. The 3-arg internal overload used by tests for deterministic injection is unchanged -- rubric tests stay isolated from cache state. - Cache key is the explicit path string, or a sentinel for the null default-search case. ConcurrentDictionary for thread-safety across parallel deconvolutions. - Cache invalidation is 'never' by design: the cached 'validated' state is an optimization, not a correctness contract. If the exe gets deleted at runtime, Process.Start surfaces the failure with the same error, just deferred. Internal Clear() exists only for test isolation. - 4 new tests in section E0 of TestRealFLASHDeconvolutionInfrastructure: Register valid / missing / null-or-empty inputs, plus an end-to-end test that registers a path, deletes the file, and asserts Deconvolute still proceeds (proves the cache is consulted, not the live filesystem).
1 parent d194d06 commit d639fdf

3 files changed

Lines changed: 194 additions & 1 deletion

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.IO;
4+
5+
namespace MassSpectrometry
6+
{
7+
/// <summary>
8+
/// Static registry of validated FLASHDeconv executable paths.
9+
///
10+
/// Each <see cref="Deconvoluter.Deconvolute(MzSpectrum, DeconvolutionParameters, MzLibUtil.MzRange)"/>
11+
/// call constructs a fresh <see cref="RealFLASHDeconvolutionAlgorithm"/>, and
12+
/// resolving the FLASHDeconv exe path costs File.Exists syscalls (the explicit
13+
/// path, plus a walk of well-known paths and the PATH env var on misses).
14+
/// On a per-scan hot path that's a few-hundred-ms stack of redundant syscalls
15+
/// per minute. The exe doesn't move between calls, so first-call validation
16+
/// is enough -- this registry caches the resolved path so subsequent calls
17+
/// skip the filesystem.
18+
///
19+
/// Production callers that know the path up front (typically MetaMorpheus via
20+
/// <c>GlobalSettings.FLASHDeconvExecutablePath</c>) should call
21+
/// <see cref="Register"/> once at startup. Ad-hoc callers don't have to do
22+
/// anything: the algorithm caches lazily on first resolution either way.
23+
/// </summary>
24+
public static class FlashDeconvExePathRegistry
25+
{
26+
// Sentinel key for "no explicit path was supplied; this is the result of
27+
// walking the well-known list + PATH". A real path string can never collide
28+
// with this because the inner '<' / '>' aren't legal in filesystem paths.
29+
internal const string DefaultSearchSentinel = "<default-search>";
30+
31+
private static readonly ConcurrentDictionary<string, string> _validated
32+
= new ConcurrentDictionary<string, string>();
33+
34+
/// <summary>
35+
/// Validate the given exe path once and cache it so subsequent
36+
/// deconvolution calls skip the filesystem check.
37+
/// </summary>
38+
/// <exception cref="ArgumentException">path is null/whitespace.</exception>
39+
/// <exception cref="FileNotFoundException">path does not exist on disk.</exception>
40+
public static void Register(string path)
41+
{
42+
if (string.IsNullOrWhiteSpace(path))
43+
throw new ArgumentException("Path must be a non-empty string.", nameof(path));
44+
if (!File.Exists(path))
45+
throw new FileNotFoundException(
46+
$"FLASHDeconv not found at: {path}", path);
47+
_validated[path] = path;
48+
}
49+
50+
/// <summary>
51+
/// Number of entries currently cached. Useful for tests that want to
52+
/// assert registration happened without depending on internal state.
53+
/// </summary>
54+
public static int Count => _validated.Count;
55+
56+
/// <summary>
57+
/// Forget every cached path. Test-only -- production code has no reason
58+
/// to clear because validation is permissive (a stale cached entry just
59+
/// pushes the actual existence check to Process.Start, which surfaces
60+
/// the same failure with a clear error).
61+
/// </summary>
62+
internal static void Clear() => _validated.Clear();
63+
64+
/// <summary>
65+
/// Look up a previously-validated resolution. Key is the explicit path
66+
/// string, or the default-search sentinel when <paramref name="explicitPath"/>
67+
/// is null/whitespace (i.e. the caller wants the algorithm to search
68+
/// well-known paths + PATH itself).
69+
/// </summary>
70+
internal static bool TryGet(string? explicitPath, out string resolved)
71+
{
72+
string key = string.IsNullOrWhiteSpace(explicitPath)
73+
? DefaultSearchSentinel
74+
: explicitPath!;
75+
return _validated.TryGetValue(key, out resolved!);
76+
}
77+
78+
/// <summary>
79+
/// Cache a resolution that has already been validated (typically by the
80+
/// algorithm's own resolve pass). Keyed identically to
81+
/// <see cref="TryGet"/>: explicit path or default-search sentinel.
82+
/// </summary>
83+
internal static void CacheValidated(string? explicitPath, string resolved)
84+
{
85+
string key = string.IsNullOrWhiteSpace(explicitPath)
86+
? DefaultSearchSentinel
87+
: explicitPath!;
88+
_validated[key] = resolved;
89+
}
90+
}
91+
}

mzLib/MassSpectrometry/Deconvolution/Algorithms/RealFLASHDeconvolutionAlgorithm.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,31 @@ internal static Dictionary<int, List<IsotopicEnvelope>> DeconvoluteFile(
173173

174174
// ── Exe resolution ────────────────────────────────────────────────────
175175

176+
/// <summary>
177+
/// Production-side resolver: consult the static registry first; on miss,
178+
/// run the full validate-and-resolve pass and cache the result so the
179+
/// next per-scan call skips the filesystem.
180+
///
181+
/// MetaMorpheus / other consumers can prime the registry up front via
182+
/// <see cref="FlashDeconvExePathRegistry.Register"/>, in which case this
183+
/// method becomes a single dictionary lookup per call.
184+
/// </summary>
176185
private static string ResolveExePath(string? explicitPath)
177-
=> ResolveExePath(
186+
{
187+
if (FlashDeconvExePathRegistry.TryGet(explicitPath, out string cached))
188+
return cached;
189+
190+
string resolved = ResolveExePath(
178191
explicitPath,
179192
DefaultWellKnownPaths,
180193
Environment.GetEnvironmentVariable("PATH"));
181194

195+
// The inner overload already verified File.Exists during the resolve;
196+
// store the result so the next call doesn't re-check.
197+
FlashDeconvExePathRegistry.CacheValidated(explicitPath, resolved);
198+
return resolved;
199+
}
200+
182201
/// <summary>
183202
/// Test-friendly overload that takes the well-known-paths list and PATH-env
184203
/// value as parameters instead of reading them from compiled-in defaults +

mzLib/Test/Deconvolution/TestRealFLASHDeconvolutionInfrastructure.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,89 @@ public void ParseSpecTsvByScan_RowsWithUnparseableRequiredCells_AreSilentlySkipp
598598
}
599599
}
600600

601+
// ── E0: FlashDeconvExePathRegistry (caching layer) ────────────────────
602+
603+
[Test]
604+
public void FlashDeconvExePathRegistry_RegisterValidPath_AddsEntryAndCounts()
605+
{
606+
FlashDeconvExePathRegistry.Clear();
607+
string fake = CreateFakeExeFile();
608+
try
609+
{
610+
Assert.That(FlashDeconvExePathRegistry.Count, Is.EqualTo(0));
611+
FlashDeconvExePathRegistry.Register(fake);
612+
Assert.That(FlashDeconvExePathRegistry.Count, Is.EqualTo(1));
613+
}
614+
finally
615+
{
616+
FlashDeconvExePathRegistry.Clear();
617+
if (File.Exists(fake)) File.Delete(fake);
618+
}
619+
}
620+
621+
[Test]
622+
public void FlashDeconvExePathRegistry_RegisterMissingPath_ThrowsFileNotFound()
623+
{
624+
FlashDeconvExePathRegistry.Clear();
625+
string nonexistent = Path.Combine(Path.GetTempPath(),
626+
$"realflash_missing_{Guid.NewGuid():N}.exe");
627+
628+
Assert.That(
629+
() => FlashDeconvExePathRegistry.Register(nonexistent),
630+
Throws.TypeOf<FileNotFoundException>());
631+
Assert.That(FlashDeconvExePathRegistry.Count, Is.EqualTo(0),
632+
"Failed registration must not leak a partial entry into the registry.");
633+
}
634+
635+
[Test]
636+
public void FlashDeconvExePathRegistry_RegisterNullOrEmpty_ThrowsArgumentException()
637+
{
638+
Assert.That(() => FlashDeconvExePathRegistry.Register(null!),
639+
Throws.TypeOf<ArgumentException>());
640+
Assert.That(() => FlashDeconvExePathRegistry.Register(""),
641+
Throws.TypeOf<ArgumentException>());
642+
Assert.That(() => FlashDeconvExePathRegistry.Register(" "),
643+
Throws.TypeOf<ArgumentException>());
644+
}
645+
646+
[Test]
647+
public void Deconvolute_WithRegisteredPath_SkipsFilesystemValidation()
648+
{
649+
// Pin the optimization: once a path is in the registry, the algorithm
650+
// uses it without re-checking File.Exists. Prove the cache is being
651+
// consulted by registering the path, deleting the file, and observing
652+
// that Deconvolute still proceeds to the stub runner -- a non-cached
653+
// resolve would throw FileNotFoundException at this point.
654+
FlashDeconvExePathRegistry.Clear();
655+
string fakeExe = CreateFakeExeFile();
656+
FlashDeconvExePathRegistry.Register(fakeExe);
657+
File.Delete(fakeExe); // cache says it exists; filesystem says otherwise
658+
659+
try
660+
{
661+
var p = new RealFLASHDeconvolutionParameters(
662+
flashDeconvExePath: fakeExe,
663+
minMass: 50, maxMass: 5000);
664+
665+
string tsv = string.Join("\n",
666+
Ms1TsvHeader,
667+
"1\t1000.0\t5.0e5\t5\t201.0\t201.4\t0.95\t12.0\t0.8");
668+
669+
var algorithm = new RealFLASHDeconvolutionAlgorithm(p, BuildStubRunner(tsv));
670+
671+
var result = algorithm
672+
.Deconvolute(BuildSyntheticSpectrum(), new MzRange(0, 5000))
673+
.ToList();
674+
675+
Assert.That(result, Has.Count.EqualTo(1),
676+
"Cached path must be honored without re-validating that the file still exists; the deleted file would otherwise throw before the stub runner runs.");
677+
}
678+
finally
679+
{
680+
FlashDeconvExePathRegistry.Clear();
681+
}
682+
}
683+
601684
// ── E: ResolveExePath unit tests ──────────────────────────────────────
602685

603686
[Test]

0 commit comments

Comments
 (0)