mirror of
https://github.com/FroggMaster/CreamInstaller.git
synced 2026-07-28 14:47:04 -07:00
Scan and Logging Performance Improvements
- Parallelized Steam ACF and DLC scanning - Replaced lock-based logging with async channel to avoid UI locks during parallel thread calls. - Added MD5 cache during file checks to reduce redundant I/O calls
This commit is contained in:
@@ -657,12 +657,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
}
|
||||
|
||||
Stopwatch gameDlcTimer = Stopwatch.StartNew();
|
||||
foreach (Task task in appTasks)
|
||||
{
|
||||
if (Program.Canceled)
|
||||
return;
|
||||
await task;
|
||||
}
|
||||
await Task.WhenAll(appTasks);
|
||||
gameDlcTimer.Stop();
|
||||
|
||||
gameQueriesDone.TrySetResult();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -29,10 +30,10 @@ internal static class SteamLibrary
|
||||
|
||||
internal static async Task<List<(string appId, string name, string branch, int buildId, string gameDirectory)>>
|
||||
GetGames()
|
||||
=> await Task.Run(async () =>
|
||||
{
|
||||
Stopwatch timer = Stopwatch.StartNew();
|
||||
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
|
||||
HashSet<string> seenAppIds = new();
|
||||
HashSet<string> gameLibraryDirectories = await GetLibraryDirectories();
|
||||
ProgramData.Log($"[Steam] Found {gameLibraryDirectories.Count} library folder(s).");
|
||||
foreach (string libraryDirectory in gameLibraryDirectories)
|
||||
@@ -40,41 +41,46 @@ internal static class SteamLibrary
|
||||
if (Program.Canceled)
|
||||
return games;
|
||||
ProgramData.Log($"[Steam] Scanning library: {libraryDirectory}");
|
||||
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) game in (await
|
||||
GetGamesFromLibraryDirectory(
|
||||
libraryDirectory)).Where(game => games.All(_game => _game.appId != game.appId)))
|
||||
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) game in
|
||||
await GetGamesFromLibraryDirectory(libraryDirectory))
|
||||
{
|
||||
if (seenAppIds.Add(game.appId))
|
||||
games.Add(game);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) testGame in
|
||||
TestGames.Where(t => games.All(g => g.appId != t.appId)))
|
||||
TestGames.Where(t => !seenAppIds.Contains(t.appId)))
|
||||
games.Add(testGame);
|
||||
if (TestGames.Count > 0)
|
||||
ProgramData.Log($"[Steam] Injected {TestGames.Count} test game(s).");
|
||||
timer.Stop();
|
||||
ProgramData.Log($"[Steam] Total games detected: {games.Count} in {(timer.Elapsed.TotalSeconds >= 60 ? $"{timer.Elapsed.TotalSeconds / 60:F1} minutes" : $"{timer.Elapsed.TotalSeconds:F1}s")}");
|
||||
return games;
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<List<(string appId, string name, string branch, int buildId, string gameDirectory)>>
|
||||
GetGamesFromLibraryDirectory(string libraryDirectory)
|
||||
=> await Task.Run(() =>
|
||||
{
|
||||
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
|
||||
if (Program.Canceled || !libraryDirectory.DirectoryExists())
|
||||
{
|
||||
ProgramData.Log($"[Steam] Skipping library (not found or canceled): {libraryDirectory}");
|
||||
return games;
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach (string file in libraryDirectory.EnumerateDirectory("*.acf"))
|
||||
ConcurrentDictionary<string, (string name, string branch, int buildId, string gameDirectory)> gamesDict = new();
|
||||
Parallel.ForEach(libraryDirectory.EnumerateDirectory("*.acf"), (file, state) =>
|
||||
{
|
||||
if (Program.Canceled)
|
||||
return games;
|
||||
{
|
||||
state.Stop();
|
||||
return;
|
||||
}
|
||||
if (!ValveDataFile.TryDeserialize(file.ReadFile(), out VProperty result))
|
||||
{
|
||||
ProgramData.Log($"[Steam] Failed to deserialize ACF: {file}");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
string appId = result.Value.GetChild("appid")?.ToString();
|
||||
@@ -82,11 +88,10 @@ internal static class SteamLibrary
|
||||
string name = result.Value.GetChild("name")?.ToString();
|
||||
string buildId = result.Value.GetChild("buildid")?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(installdir) ||
|
||||
string.IsNullOrWhiteSpace(name)
|
||||
|| string.IsNullOrWhiteSpace(buildId))
|
||||
string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(buildId))
|
||||
{
|
||||
ProgramData.Log($"[Steam] Skipping ACF with missing fields: {file}");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
string rawGameDirectory = libraryDirectory + @"\common\" + installdir;
|
||||
@@ -94,12 +99,11 @@ internal static class SteamLibrary
|
||||
if (gameDirectory is null)
|
||||
{
|
||||
ProgramData.Log($"[Steam] Game directory not found (drive may be slow or disconnected): {rawGameDirectory} | App: {name} ({appId})");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(appId, out int _) || !int.TryParse(buildId, out int buildIdInt) ||
|
||||
games.Any(g => g.appId == appId))
|
||||
continue;
|
||||
if (!int.TryParse(appId, out int _) || !int.TryParse(buildId, out int buildIdInt))
|
||||
return;
|
||||
|
||||
VToken userConfig = result.Value.GetChild("UserConfig");
|
||||
string branch = userConfig?.GetChild("BetaKey")?.ToString();
|
||||
@@ -114,10 +118,13 @@ internal static class SteamLibrary
|
||||
if (string.IsNullOrWhiteSpace(branch))
|
||||
branch = "public";
|
||||
|
||||
if (gamesDict.TryAdd(appId, (name, branch, buildIdInt, gameDirectory)))
|
||||
ProgramData.Log($"[Steam] Detected game: {name} ({appId}) | Branch: {branch} | Dir: {gameDirectory}");
|
||||
games.Add((appId, name, branch, buildIdInt, gameDirectory));
|
||||
}
|
||||
});
|
||||
|
||||
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new(gamesDict.Count);
|
||||
foreach (KeyValuePair<string, (string name, string branch, int buildId, string gameDirectory)> kv in gamesDict)
|
||||
games.Add((kv.Key, kv.Value.name, kv.Value.branch, kv.Value.buildId, kv.Value.gameDirectory));
|
||||
return games;
|
||||
});
|
||||
|
||||
|
||||
@@ -111,19 +111,7 @@ internal static class Resources
|
||||
!rootDirectory.IsCommonIncorrectExecutable(path))
|
||||
&& (validFunc is null || validFunc(path)) &&
|
||||
path.TryGetFileBinaryType(out BinaryType binaryType) &&
|
||||
binaryType is BinaryType.BIT64)
|
||||
executables.Add((path, binaryType));
|
||||
}
|
||||
|
||||
foreach (string path in rootDirectory.EnumerateDirectory("*.exe", true))
|
||||
{
|
||||
if (Program.Canceled)
|
||||
return null;
|
||||
if (executables.All(e => e.path != path) && (!filterCommon ||
|
||||
!rootDirectory.IsCommonIncorrectExecutable(path))
|
||||
&& (validFunc is null || validFunc(path)) &&
|
||||
path.TryGetFileBinaryType(out BinaryType binaryType) &&
|
||||
binaryType is BinaryType.BIT32)
|
||||
binaryType is BinaryType.BIT64 or BinaryType.BIT32)
|
||||
executables.Add((path, binaryType));
|
||||
}
|
||||
|
||||
@@ -202,11 +190,19 @@ internal static class Resources
|
||||
return dllDirectories.Count > 0 ? dllDirectories : null;
|
||||
});
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> Md5HashCache = new();
|
||||
|
||||
#pragma warning disable CA5351
|
||||
private static string ComputeMD5(this string filePath)
|
||||
=> filePath.FileExists() && filePath.ReadFileBytes(true) is { } bytes
|
||||
{
|
||||
if (Md5HashCache.TryGetValue(filePath, out string cached))
|
||||
return cached;
|
||||
string hash = filePath.FileExists() && filePath.ReadFileBytes(true) is { } bytes
|
||||
? BitConverter.ToString(MD5.HashData(bytes)).Replace("-", "").ToUpperInvariant()
|
||||
: null;
|
||||
Md5HashCache[filePath] = hash;
|
||||
return hash;
|
||||
}
|
||||
#pragma warning restore CA5351
|
||||
|
||||
internal static bool IsResourceFile(this string filePath, ResourceIdentifier identifier)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CreamInstaller;
|
||||
@@ -81,16 +82,69 @@ internal static event Action<string> OnLogSteam;
|
||||
internal static event Action<string> OnLogWarning;
|
||||
internal static event Action<string> OnLogError;
|
||||
|
||||
private static readonly object LogLock = new();
|
||||
private static string FormatLogEntry(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
return $"[{timestamp}] {message}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
private static string FormatLogErrorEntry(string message, Exception ex)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
return $"[{timestamp}] [ERROR] {message}{Environment.NewLine}[{timestamp}] [ERROR] Exception: {ex}{Environment.NewLine}";
|
||||
}
|
||||
|
||||
private readonly record struct LogEntry(string Path, string Text);
|
||||
|
||||
private static readonly Channel<LogEntry> LogChannel = Channel.CreateUnbounded<LogEntry>();
|
||||
private static readonly Task LogConsumer;
|
||||
|
||||
static ProgramData()
|
||||
{
|
||||
LogConsumer = ConsumeLogAsync();
|
||||
}
|
||||
|
||||
private static async Task ConsumeLogAsync()
|
||||
{
|
||||
ChannelReader<LogEntry> reader = LogChannel.Reader;
|
||||
List<LogEntry> batch = new(64);
|
||||
while (await reader.WaitToReadAsync())
|
||||
{
|
||||
batch.Clear();
|
||||
while (reader.TryRead(out LogEntry entry))
|
||||
{
|
||||
batch.Add(entry);
|
||||
if (batch.Count >= 64)
|
||||
break;
|
||||
}
|
||||
if (batch.Count > 0)
|
||||
FlushLogBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FlushLogBatch(List<LogEntry> entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (IGrouping<string, LogEntry> group in entries.GroupBy(e => e.Path))
|
||||
{
|
||||
StringBuilder combined = new(group.Sum(e => e.Text.Length));
|
||||
foreach (LogEntry entry in group)
|
||||
_ = combined.Append(entry.Text);
|
||||
File.AppendAllText(group.Key, combined.ToString(), Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored; logging must never crash the application
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Log(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
string entry = $"[{timestamp}] {message}{Environment.NewLine}";
|
||||
lock (LogLock)
|
||||
File.AppendAllText(ScanLogPath, entry, Encoding.UTF8);
|
||||
LogChannel.Writer.TryWrite(new LogEntry(ScanLogPath, FormatLogEntry(message)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -103,10 +157,7 @@ internal static event Action<string> OnLogError;
|
||||
{
|
||||
try
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
string entry = $"[{timestamp}] {message}{Environment.NewLine}";
|
||||
lock (LogLock)
|
||||
File.AppendAllText(SteamLogPath, entry, Encoding.UTF8);
|
||||
LogChannel.Writer.TryWrite(new LogEntry(SteamLogPath, FormatLogEntry(message)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -119,10 +170,7 @@ internal static event Action<string> OnLogError;
|
||||
{
|
||||
try
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
string entry = $"[{timestamp}] [WARN] {message}{Environment.NewLine}";
|
||||
lock (LogLock)
|
||||
File.AppendAllText(AppLogPath, entry, Encoding.UTF8);
|
||||
LogChannel.Writer.TryWrite(new LogEntry(AppLogPath, FormatLogEntry($"[WARN] {message}")));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -135,12 +183,10 @@ internal static event Action<string> OnLogError;
|
||||
{
|
||||
try
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
string entry = ex is not null
|
||||
? $"[{timestamp}] [ERROR] {message}{Environment.NewLine}[{timestamp}] [ERROR] Exception: {ex}{Environment.NewLine}"
|
||||
: $"[{timestamp}] [ERROR] {message}{Environment.NewLine}";
|
||||
lock (LogLock)
|
||||
File.AppendAllText(AppLogPath, entry, Encoding.UTF8);
|
||||
? FormatLogErrorEntry(message, ex)
|
||||
: FormatLogEntry($"[ERROR] {message}");
|
||||
LogChannel.Writer.TryWrite(new LogEntry(AppLogPath, entry));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -188,13 +234,25 @@ internal static event Action<string> OnLogError;
|
||||
OldProgramChoicesPath.DeleteFile();
|
||||
});
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, DateTime> CooldownCache = new();
|
||||
|
||||
internal static bool CheckCooldown(string identifier, int cooldown)
|
||||
{
|
||||
DateTime now = DateTime.UtcNow;
|
||||
DateTime lastCheck = GetCooldown(identifier) ?? now;
|
||||
bool cooldownOver = (now - lastCheck).TotalSeconds > cooldown;
|
||||
if (cooldownOver || now == lastCheck)
|
||||
|
||||
if (CooldownCache.TryGetValue(identifier, out DateTime cached) &&
|
||||
(now - cached).TotalSeconds <= cooldown)
|
||||
return false;
|
||||
|
||||
DateTime? lastCheck = GetCooldown(identifier);
|
||||
DateTime effective = lastCheck ?? now;
|
||||
bool cooldownOver = (now - effective).TotalSeconds > cooldown;
|
||||
|
||||
CooldownCache[identifier] = now;
|
||||
|
||||
if (cooldownOver || now == effective)
|
||||
SetCooldown(identifier, now);
|
||||
|
||||
return cooldownOver;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user