From 7a42dbbf14494e18c6c67bf4b20b8eb69bfcecfc Mon Sep 17 00:00:00 2001 From: kuba Date: Tue, 29 Sep 2026 23:58:13 +0200 Subject: [PATCH] [Kernel] Memoize guest path resolution against the mount configuration Resolving a guest path to a host path is pure text work plus a reparse-point check per path component, but a resource streamer calls sceKernelAprResolveFilepathsToIdsAndFileSizes with the same paths hundreds of thousands of times over a load, so the work dominates. Successful resolutions are now memoized. The only inputs that can change an already-computed answer are the mount table and the host mount roots, so each entry records the root configuration it was computed under and is ignored if that has changed, and registering or removing a guest mount clears the cache outright. A denial is not memoized: it is a containment decision about the host filesystem's current shape, so it stays live. The cache is bounded, and its size is tracked in a plain counter because ConcurrentDictionary.Count takes every bucket lock, which on a path a dozen guest threads resolve at once costs more than the resolution it guards. --- .../Kernel/KernelMemoryCompatExports.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index fd59b42..817621d 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -235,6 +235,8 @@ public static partial class KernelMemoryCompatExports _guestMounts[normalizedMountPoint] = normalizedHostRoot; } + InvalidateResolvedGuestPaths(); + lock (_statCacheGate) { _negativeStatCache.RemoveWhere(path => @@ -252,10 +254,18 @@ public static partial class KernelMemoryCompatExports return false; } + bool removed; lock (_guestMountGate) { - return _guestMounts.Remove(normalizedMountPoint); + removed = _guestMounts.Remove(normalizedMountPoint); } + + if (removed) + { + InvalidateResolvedGuestPaths(); + } + + return removed; } internal static bool TryAllocateHleData( @@ -4725,6 +4735,38 @@ public static partial class KernelMemoryCompatExports return FileMode.Open; } + // Resolution is pure text work plus a per-component reparse-point check, but a + // resource streamer resolves the same paths hundreds of thousands of times through + // sceKernelAprResolveFilepathsToIdsAndFileSizes. Memoize the mapping and drop it + // whenever the mount table changes, which is the only input that can change an + // already-computed answer. + // Each entry records the mount-root configuration it was computed under, so a + // host configuration change cannot serve a stale answer. + private static readonly ConcurrentDictionary _resolvedGuestPaths = + new(StringComparer.Ordinal); + private const int ResolvedGuestPathCacheLimit = 1 << 20; + // ConcurrentDictionary.Count takes every bucket lock, which on a path resolved by + // a dozen guest threads at once costs far more than the resolution it guards. + private static int _resolvedGuestPathCount; + + internal static void InvalidateResolvedGuestPaths() + { + _resolvedGuestPaths.Clear(); + Interlocked.Exchange(ref _resolvedGuestPathCount, 0); + } + + // Every mount root a built-in branch can use. Resolution depends on nothing else + // that changes at runtime (the mount table clears the cache itself), so a memoized + // answer stays valid exactly as long as this token does. + private static string RootConfigurationToken() => + string.Concat( + Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"), "|", + Environment.GetEnvironmentVariable("SHARPEMU_HOSTAPP_DIR"), "|", + Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR"), "|", + Environment.GetEnvironmentVariable("SHARPEMU_TEMP0_DIR"), "|", + Environment.GetEnvironmentVariable("SHARPEMU_DOWNLOAD0_DIR"), "|", + Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR")); + public static string ResolveGuestPath(string guestPath) { if (string.IsNullOrWhiteSpace(guestPath)) @@ -4732,6 +4774,28 @@ public static partial class KernelMemoryCompatExports return guestPath; } + var roots = RootConfigurationToken(); + if (_resolvedGuestPaths.TryGetValue(guestPath, out var memoized) && + string.Equals(memoized.Root, roots, StringComparison.Ordinal)) + { + return memoized.Path; + } + + var resolved = ResolveGuestPathUncached(guestPath); + // Only a successful resolution is memoized: a denial is a containment + // decision about the host filesystem's current shape, so it stays live. + if (!string.IsNullOrEmpty(resolved) && + Volatile.Read(ref _resolvedGuestPathCount) < ResolvedGuestPathCacheLimit && + _resolvedGuestPaths.TryAdd(guestPath, (roots, resolved))) + { + Interlocked.Increment(ref _resolvedGuestPathCount); + } + + return resolved; + } + + private static string ResolveGuestPathUncached(string guestPath) + { if (TryResolveRegisteredGuestMount(guestPath, out var mountedPath, out var mountPrefixMatched)) { return mountedPath;