Speed up library refresh and show scan progress

The refresh walked every file under each folder - including entire game dumps - while the status bar stayed silent until the first source was read. Discovery is now a pruned depth-first walk: a loose-dump root (sce_sys/param.json) is recorded and its content is not descended into, and the app's own scratch directories (PS5PKGTool*, .PS5PKGTool*) are skipped, so a workspace that sits inside a library folder is no longer scanned as game content. Discovery reports the directory being walked (throttled), and the status bar shows a themed marquee progress bar before the live text so a refresh is visibly working from the moment it starts.
This commit is contained in:
Izat Emir committed 2026-10-11 16:09:03 +08:00
1 parent f8f63d4f7d
commit 5695c4e6e9
5 files changed
+95 -18

No files matched your search

+46 -11
View File
@@ -14,10 +14,13 @@ public sealed class Ps5DumpLocator
/// <summary>
/// Enumerates <paramref name="selectedPath"/> once and classifies every file by extension
/// (plus <c>sce_sys/param.json</c> loose dumps). Replaces the per-extension walks so a large
/// library tree is traversed a single time.
/// (plus <c>sce_sys/param.json</c> loose dumps). The walk is pruned at each loose-dump root:
/// a directory holding <c>sce_sys/param.json</c> is a game dump whose content can never itself
/// be a source, so it is recorded and not descended into (a dump holds tens of thousands of
/// files). <paramref name="onDirectory"/> is invoked for each directory visited, unthrottled.
/// </summary>
public LocatedSources FindAll(string selectedPath, bool recursive, CancellationToken cancellationToken = default)
public LocatedSources FindAll(string selectedPath, bool recursive, CancellationToken cancellationToken = default,
Action<string>? onDirectory = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(selectedPath);
var found = new LocatedSources();
@@ -41,22 +44,43 @@ public sealed class Ps5DumpLocator
if (!Directory.Exists(selectedPath)) return found;
// A dump root is identified by sce_sys/param.json directly under it; check this explicitly
// so non-recursive scans still pick up a dump passed as the selected folder.
string directParam = Path.Combine(selectedPath, "sce_sys", "param.json");
if (File.Exists(directParam)) found.ParamFiles.Add(Path.GetFullPath(directParam));
var options = new EnumerationOptions
{
RecurseSubdirectories = recursive,
IgnoreInaccessible = true,
AttributesToSkip = FileAttributes.ReparsePoint,
MatchCasing = MatchCasing.CaseInsensitive
};
foreach (string path in Directory.EnumerateFiles(selectedPath, "*", options))
var pending = new Stack<string>();
pending.Push(Path.GetFullPath(selectedPath));
while (pending.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested();
Classify(Path.GetFullPath(path), found);
string directory = pending.Pop();
onDirectory?.Invoke(directory);
// A dump root is identified by sce_sys/param.json directly under it. Record it and stop:
// the dump content below it cannot hold another source worth walking.
string dumpParam = Path.Combine(directory, "sce_sys", "param.json");
if (File.Exists(dumpParam))
{
found.ParamFiles.Add(Path.GetFullPath(dumpParam));
continue;
}
// One directory read per directory (attributes come from the scan, so classifying a file
// or a subdirectory costs no extra I/O).
foreach (FileSystemInfo entry in new DirectoryInfo(directory).EnumerateFileSystemInfos("*", options))
{
cancellationToken.ThrowIfCancellationRequested();
if ((entry.Attributes & FileAttributes.Directory) != 0)
{
if (recursive && !IsScratchDirectory(entry.Name)) pending.Push(entry.FullName);
}
else
{
Classify(entry.FullName, found);
}
}
}
Normalize(found);
@@ -81,6 +105,17 @@ public sealed class Ps5DumpLocator
else if (extension.Equals(".ffpkg", StringComparison.OrdinalIgnoreCase)) found.Ffpkg.Add(fullPath);
}
/// <summary>
/// True for a directory the application itself creates as scratch space: image/convert staging
/// (<c>PS5PKGTool-image-&lt;guid&gt;</c>, <c>PS5PKGTool\pkg-image</c>, <c>PS5PKGTool\convert</c>),
/// exFAT/FFPKG edit workspaces (<c>.PS5PKGTool-exfat-edit-*</c>, <c>.PS5PKGTool-ffpkg-edit-*</c>)
/// and preview output. When the workspace sits inside a library folder these would otherwise be
/// walked as if they were game content.
/// </summary>
internal static bool IsScratchDirectory(string name) =>
name.StartsWith("PS5PKGTool", StringComparison.OrdinalIgnoreCase) ||
name.StartsWith(".PS5PKGTool", StringComparison.OrdinalIgnoreCase);
private static void Normalize(LocatedSources found)
{
Deduplicate(found.ParamFiles);
+16 -1
View File
@@ -1,5 +1,6 @@
using PS5PKGTool.Core.Models;
using PS5PKGTool.Core.Parsers;
using System.Diagnostics;
using System.Text.Json;
namespace PS5PKGTool.Core.Services;
@@ -9,6 +10,8 @@ public sealed class Ps5ScanProgress
public int Processed { get; init; }
public int Total { get; init; }
public string CurrentPath { get; init; } = string.Empty;
/// <summary>True while the source discovery walk runs (Total is not yet known).</summary>
public bool IsDiscovery { get; init; }
}
public sealed class Ps5ScanResult
@@ -40,13 +43,25 @@ public sealed class Ps5LibraryScanner
var ffpfscPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var filesystemImagePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var ffpkgPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Report the current directory during discovery, throttled so the UI thread is never flooded
// (the walk can visit tens of thousands of directories before the first source is read).
var discoveryWatch = Stopwatch.StartNew();
long lastDiscoveryReport = -1000;
void ReportDiscovery(string directory)
{
long now = discoveryWatch.ElapsedMilliseconds;
if (now - lastDiscoveryReport < 250) return;
lastDiscoveryReport = now;
progress?.Report(new Ps5ScanProgress { IsDiscovery = true, CurrentPath = directory });
}
foreach (string folder in libraryFolders.Where(value => !string.IsNullOrWhiteSpace(value)))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// One traversal per folder; every candidate kind is classified from the same walk.
Ps5DumpLocator.LocatedSources located = _locator.FindAll(folder, recursive, cancellationToken);
Ps5DumpLocator.LocatedSources located = _locator.FindAll(folder, recursive, cancellationToken, ReportDiscovery);
foreach (string path in located.ParamFiles) paramPaths.Add(path);
foreach (string path in located.Packages) packagePaths.Add(path);
foreach (string path in located.Ffpfsc) ffpfscPaths.Add(path);
+16 -5
View File
@@ -380,10 +380,11 @@ partial class MainForm
private DarkUI.Controls.DarkCheckBox chkImageAdvancedOptions = null!;
private DarkUI.Controls.DarkLabel lblImageTemp = null!;
private SaveFileDialog imageSaveDialog = null!;
private DarkUI.Controls.DarkStatusStrip statusMain = null!;
private DarkUI.Controls.DarkToolStripStatusLabel statusLabel = null!;
private DarkUI.Controls.DarkToolStripStatusLabel statusSpring = null!;
private DarkUI.Controls.DarkToolStripStatusLabel statusCount = null!;
private DarkUI.Controls.DarkStatusStrip statusMain = null!;
private DarkUI.Controls.DarkToolStripStatusLabel statusLabel = null!;
private DarkUI.Controls.DarkToolStripProgressBar statusBusy = null!;
private DarkUI.Controls.DarkToolStripStatusLabel statusSpring = null!;
private DarkUI.Controls.DarkToolStripStatusLabel statusCount = null!;
private FolderBrowserDialog folderBrowserDialog = null!;
private OpenFileDialog packageOpenDialog = null!;
private OpenFileDialog sourceImageOpenDialog = null!;
@@ -776,6 +777,7 @@ partial class MainForm
trophyCsvSaveDialog = new SaveFileDialog();
statusMain = new DarkUI.Controls.DarkStatusStrip();
statusLabel = new DarkUI.Controls.DarkToolStripStatusLabel();
statusBusy = new DarkUI.Controls.DarkToolStripProgressBar();
statusSpring = new DarkUI.Controls.DarkToolStripStatusLabel();
statusCount = new DarkUI.Controls.DarkToolStripStatusLabel();
folderBrowserDialog = new FolderBrowserDialog();
@@ -4881,7 +4883,7 @@ partial class MainForm
//
// statusMain
//
statusMain.Items.AddRange(new ToolStripItem[] { statusLabel, statusSpring, statusCount });
statusMain.Items.AddRange(new ToolStripItem[] { statusBusy, statusLabel, statusSpring, statusCount });
statusMain.Location = new Point(0, 851);
statusMain.Name = "statusMain";
statusMain.Padding = new Padding(0, 4, 0, 4);
@@ -4895,6 +4897,15 @@ partial class MainForm
statusLabel.Size = new Size(39, 17);
statusLabel.Text = "Ready";
//
// statusBusy
//
statusBusy.Marquee = true;
statusBusy.MarqueeAnimationSpeed = 30;
statusBusy.Margin = new Padding(6, 3, 6, 3);
statusBusy.Name = "statusBusy";
statusBusy.Size = new Size(120, 14);
statusBusy.Visible = false;
//
// statusSpring
//
statusSpring.BackColor = Color.FromArgb(60, 63, 65);
+1
View File
@@ -214,6 +214,7 @@ public partial class MainForm
if (!_taskRefreshPending || IsDisposed) return;
_taskRefreshPending = false;
RefreshTaskGrid();
UpdateOperationState();
}
private void RefreshTaskGrid()
+16 -1
View File
@@ -431,8 +431,20 @@ public partial class MainForm : DarkForm
// the newer scan's busy state when it finishes.
int generation = ++_scanGeneration;
SetScanning(true);
statusLabel.Text = "Scanning for PS5 dumps, packages, and filesystem images...";
var progress = new Progress<Ps5ScanProgress>(value =>
{
if (value.IsDiscovery)
{
// Discovery walk: show the directory being scanned so a long walk never looks idle.
// Trim a long path so it cannot push the game count off the status bar.
string path = value.CurrentPath;
if (path.Length > 100) path = "..." + path[^100..];
statusLabel.Text = path.Length == 0
? "Scanning for PS5 dumps, packages, and filesystem images..."
: $"Scanning {path}";
return;
}
// The final report has Processed == Total (and no current path); do not show N+1 of N.
statusLabel.Text = value.Total == 0
? "Searching for PS5 dumps, packages, and filesystem images..."
@@ -496,13 +508,16 @@ public partial class MainForm : DarkForm
private void UpdateOperationState()
{
bool idle = !_isScanning && !_isFileBusy;
bool tasksRunning = _taskQueue.Tasks.Any(task => task.Status is PackageTaskStatus.Running or PackageTaskStatus.Queued);
// The status-bar marquee runs whenever the app is doing anything that blocks or backgrounds work.
statusBusy.Visible = !idle || tasksRunning;
menuAddFolder.Enabled = idle;
menuOpenDump.Enabled = idle;
menuOpenPackage.Enabled = idle;
menuRefresh.Enabled = idle;
menuSettings.Enabled = idle;
btnImageRun.Enabled = idle && cboImageAction.SelectedItem is not null;
btnImageCancel.Enabled = _taskQueue.Tasks.Any(task => task.Status is PackageTaskStatus.Running or PackageTaskStatus.Queued);
btnImageCancel.Enabled = tasksRunning;
btnFileCancel.Enabled = _isFileBusy;
}