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();
|
Stopwatch gameDlcTimer = Stopwatch.StartNew();
|
||||||
foreach (Task task in appTasks)
|
await Task.WhenAll(appTasks);
|
||||||
{
|
|
||||||
if (Program.Canceled)
|
|
||||||
return;
|
|
||||||
await task;
|
|
||||||
}
|
|
||||||
gameDlcTimer.Stop();
|
gameDlcTimer.Stop();
|
||||||
|
|
||||||
gameQueriesDone.TrySetResult();
|
gameQueriesDone.TrySetResult();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -29,52 +30,57 @@ internal static class SteamLibrary
|
|||||||
|
|
||||||
internal static async Task<List<(string appId, string name, string branch, int buildId, string gameDirectory)>>
|
internal static async Task<List<(string appId, string name, string branch, int buildId, string gameDirectory)>>
|
||||||
GetGames()
|
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)
|
||||||
{
|
{
|
||||||
Stopwatch timer = Stopwatch.StartNew();
|
if (Program.Canceled)
|
||||||
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
|
return games;
|
||||||
HashSet<string> gameLibraryDirectories = await GetLibraryDirectories();
|
ProgramData.Log($"[Steam] Scanning library: {libraryDirectory}");
|
||||||
ProgramData.Log($"[Steam] Found {gameLibraryDirectories.Count} library folder(s).");
|
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) game in
|
||||||
foreach (string libraryDirectory in gameLibraryDirectories)
|
await GetGamesFromLibraryDirectory(libraryDirectory))
|
||||||
{
|
{
|
||||||
if (Program.Canceled)
|
if (seenAppIds.Add(game.appId))
|
||||||
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)))
|
|
||||||
games.Add(game);
|
games.Add(game);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) testGame in
|
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);
|
games.Add(testGame);
|
||||||
if (TestGames.Count > 0)
|
if (TestGames.Count > 0)
|
||||||
ProgramData.Log($"[Steam] Injected {TestGames.Count} test game(s).");
|
ProgramData.Log($"[Steam] Injected {TestGames.Count} test game(s).");
|
||||||
timer.Stop();
|
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")}");
|
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;
|
return games;
|
||||||
});
|
}
|
||||||
|
|
||||||
private static async Task<List<(string appId, string name, string branch, int buildId, string gameDirectory)>>
|
private static async Task<List<(string appId, string name, string branch, int buildId, string gameDirectory)>>
|
||||||
GetGamesFromLibraryDirectory(string libraryDirectory)
|
GetGamesFromLibraryDirectory(string libraryDirectory)
|
||||||
=> await Task.Run(() =>
|
=> await Task.Run(() =>
|
||||||
{
|
{
|
||||||
List<(string appId, string name, string branch, int buildId, string gameDirectory)> games = new();
|
|
||||||
if (Program.Canceled || !libraryDirectory.DirectoryExists())
|
if (Program.Canceled || !libraryDirectory.DirectoryExists())
|
||||||
{
|
{
|
||||||
ProgramData.Log($"[Steam] Skipping library (not found or canceled): {libraryDirectory}");
|
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)
|
if (Program.Canceled)
|
||||||
return games;
|
{
|
||||||
|
state.Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!ValveDataFile.TryDeserialize(file.ReadFile(), out VProperty result))
|
if (!ValveDataFile.TryDeserialize(file.ReadFile(), out VProperty result))
|
||||||
{
|
{
|
||||||
ProgramData.Log($"[Steam] Failed to deserialize ACF: {file}");
|
ProgramData.Log($"[Steam] Failed to deserialize ACF: {file}");
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string appId = result.Value.GetChild("appid")?.ToString();
|
string appId = result.Value.GetChild("appid")?.ToString();
|
||||||
@@ -82,11 +88,10 @@ internal static class SteamLibrary
|
|||||||
string name = result.Value.GetChild("name")?.ToString();
|
string name = result.Value.GetChild("name")?.ToString();
|
||||||
string buildId = result.Value.GetChild("buildid")?.ToString();
|
string buildId = result.Value.GetChild("buildid")?.ToString();
|
||||||
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(installdir) ||
|
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(installdir) ||
|
||||||
string.IsNullOrWhiteSpace(name)
|
string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(buildId))
|
||||||
|| string.IsNullOrWhiteSpace(buildId))
|
|
||||||
{
|
{
|
||||||
ProgramData.Log($"[Steam] Skipping ACF with missing fields: {file}");
|
ProgramData.Log($"[Steam] Skipping ACF with missing fields: {file}");
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string rawGameDirectory = libraryDirectory + @"\common\" + installdir;
|
string rawGameDirectory = libraryDirectory + @"\common\" + installdir;
|
||||||
@@ -94,12 +99,11 @@ internal static class SteamLibrary
|
|||||||
if (gameDirectory is null)
|
if (gameDirectory is null)
|
||||||
{
|
{
|
||||||
ProgramData.Log($"[Steam] Game directory not found (drive may be slow or disconnected): {rawGameDirectory} | App: {name} ({appId})");
|
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) ||
|
if (!int.TryParse(appId, out int _) || !int.TryParse(buildId, out int buildIdInt))
|
||||||
games.Any(g => g.appId == appId))
|
return;
|
||||||
continue;
|
|
||||||
|
|
||||||
VToken userConfig = result.Value.GetChild("UserConfig");
|
VToken userConfig = result.Value.GetChild("UserConfig");
|
||||||
string branch = userConfig?.GetChild("BetaKey")?.ToString();
|
string branch = userConfig?.GetChild("BetaKey")?.ToString();
|
||||||
@@ -114,10 +118,13 @@ internal static class SteamLibrary
|
|||||||
if (string.IsNullOrWhiteSpace(branch))
|
if (string.IsNullOrWhiteSpace(branch))
|
||||||
branch = "public";
|
branch = "public";
|
||||||
|
|
||||||
ProgramData.Log($"[Steam] Detected game: {name} ({appId}) | Branch: {branch} | Dir: {gameDirectory}");
|
if (gamesDict.TryAdd(appId, (name, branch, buildIdInt, gameDirectory)))
|
||||||
games.Add((appId, name, branch, buildIdInt, gameDirectory));
|
ProgramData.Log($"[Steam] Detected game: {name} ({appId}) | Branch: {branch} | Dir: {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;
|
return games;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -111,19 +111,7 @@ internal static class Resources
|
|||||||
!rootDirectory.IsCommonIncorrectExecutable(path))
|
!rootDirectory.IsCommonIncorrectExecutable(path))
|
||||||
&& (validFunc is null || validFunc(path)) &&
|
&& (validFunc is null || validFunc(path)) &&
|
||||||
path.TryGetFileBinaryType(out BinaryType binaryType) &&
|
path.TryGetFileBinaryType(out BinaryType binaryType) &&
|
||||||
binaryType is BinaryType.BIT64)
|
binaryType is BinaryType.BIT64 or BinaryType.BIT32)
|
||||||
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)
|
|
||||||
executables.Add((path, binaryType));
|
executables.Add((path, binaryType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,11 +190,19 @@ internal static class Resources
|
|||||||
return dllDirectories.Count > 0 ? dllDirectories : null;
|
return dllDirectories.Count > 0 ? dllDirectories : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, string> Md5HashCache = new();
|
||||||
|
|
||||||
#pragma warning disable CA5351
|
#pragma warning disable CA5351
|
||||||
private static string ComputeMD5(this string filePath)
|
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()
|
? BitConverter.ToString(MD5.HashData(bytes)).Replace("-", "").ToUpperInvariant()
|
||||||
: null;
|
: null;
|
||||||
|
Md5HashCache[filePath] = hash;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
#pragma warning restore CA5351
|
#pragma warning restore CA5351
|
||||||
|
|
||||||
internal static bool IsResourceFile(this string filePath, ResourceIdentifier identifier)
|
internal static bool IsResourceFile(this string filePath, ResourceIdentifier identifier)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using CreamInstaller;
|
using CreamInstaller;
|
||||||
@@ -81,16 +82,69 @@ internal static event Action<string> OnLogSteam;
|
|||||||
internal static event Action<string> OnLogWarning;
|
internal static event Action<string> OnLogWarning;
|
||||||
internal static event Action<string> OnLogError;
|
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)
|
internal static void Log(string message)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
LogChannel.Writer.TryWrite(new LogEntry(ScanLogPath, FormatLogEntry(message)));
|
||||||
string entry = $"[{timestamp}] {message}{Environment.NewLine}";
|
|
||||||
lock (LogLock)
|
|
||||||
File.AppendAllText(ScanLogPath, entry, Encoding.UTF8);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -103,10 +157,7 @@ internal static event Action<string> OnLogError;
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
LogChannel.Writer.TryWrite(new LogEntry(SteamLogPath, FormatLogEntry(message)));
|
||||||
string entry = $"[{timestamp}] {message}{Environment.NewLine}";
|
|
||||||
lock (LogLock)
|
|
||||||
File.AppendAllText(SteamLogPath, entry, Encoding.UTF8);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -119,10 +170,7 @@ internal static event Action<string> OnLogError;
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
LogChannel.Writer.TryWrite(new LogEntry(AppLogPath, FormatLogEntry($"[WARN] {message}")));
|
||||||
string entry = $"[{timestamp}] [WARN] {message}{Environment.NewLine}";
|
|
||||||
lock (LogLock)
|
|
||||||
File.AppendAllText(AppLogPath, entry, Encoding.UTF8);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -135,12 +183,10 @@ internal static event Action<string> OnLogError;
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
|
||||||
string entry = ex is not null
|
string entry = ex is not null
|
||||||
? $"[{timestamp}] [ERROR] {message}{Environment.NewLine}[{timestamp}] [ERROR] Exception: {ex}{Environment.NewLine}"
|
? FormatLogErrorEntry(message, ex)
|
||||||
: $"[{timestamp}] [ERROR] {message}{Environment.NewLine}";
|
: FormatLogEntry($"[ERROR] {message}");
|
||||||
lock (LogLock)
|
LogChannel.Writer.TryWrite(new LogEntry(AppLogPath, entry));
|
||||||
File.AppendAllText(AppLogPath, entry, Encoding.UTF8);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -188,13 +234,25 @@ internal static event Action<string> OnLogError;
|
|||||||
OldProgramChoicesPath.DeleteFile();
|
OldProgramChoicesPath.DeleteFile();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, DateTime> CooldownCache = new();
|
||||||
|
|
||||||
internal static bool CheckCooldown(string identifier, int cooldown)
|
internal static bool CheckCooldown(string identifier, int cooldown)
|
||||||
{
|
{
|
||||||
DateTime now = DateTime.UtcNow;
|
DateTime now = DateTime.UtcNow;
|
||||||
DateTime lastCheck = GetCooldown(identifier) ?? now;
|
|
||||||
bool cooldownOver = (now - lastCheck).TotalSeconds > cooldown;
|
if (CooldownCache.TryGetValue(identifier, out DateTime cached) &&
|
||||||
if (cooldownOver || now == lastCheck)
|
(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);
|
SetCooldown(identifier, now);
|
||||||
|
|
||||||
return cooldownOver;
|
return cooldownOver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user