Startup performance optimizations (metadata extraction and game list initialization)
Improved performance and reliability of metadata extraction by using faster directory deletion, parallel operations, and atomic game list updates. Refactored game list initialization for better memory usage and parallelized device connection and metadata updates. Logger initialization message is now more prominent. Minor UI and code cleanups included.
This commit is contained in:
232
MainForm.cs
232
MainForm.cs
@@ -36,8 +36,6 @@ namespace AndroidSideloader
|
||||
public static string CurrAPK;
|
||||
public static string CurrPCKG;
|
||||
List<UploadGame> gamesToUpload = new List<UploadGame>();
|
||||
|
||||
|
||||
public static string currremotesimple = String.Empty;
|
||||
#else
|
||||
public bool keyheld;
|
||||
@@ -46,11 +44,13 @@ namespace AndroidSideloader
|
||||
private readonly List<UploadGame> gamesToUpload = new List<UploadGame>();
|
||||
public static bool debugMode = false;
|
||||
public bool DeviceConnected = false;
|
||||
|
||||
|
||||
public static string currremotesimple = "";
|
||||
|
||||
#endif
|
||||
private Task _adbInitTask;
|
||||
private static readonly Color ColorInstalled = ColorTranslator.FromHtml("#3c91e6");
|
||||
private static readonly Color ColorUpdateAvailable = ColorTranslator.FromHtml("#4daa57");
|
||||
private static readonly Color ColorDonateGame = ColorTranslator.FromHtml("#cb9cf2");
|
||||
private static readonly Color ColorError = ColorTranslator.FromHtml("#f52f57");
|
||||
private Panel _listViewUninstallButton;
|
||||
private bool _listViewUninstallButtonHovered = false;
|
||||
private bool isGalleryView = false;
|
||||
@@ -345,9 +345,15 @@ namespace AndroidSideloader
|
||||
// Hard kill any lingering adb.exe instances to avoid port/handle conflicts
|
||||
KillAdbProcesses();
|
||||
|
||||
// Show the form immediately
|
||||
this.Show();
|
||||
Application.DoEvents();
|
||||
// ADB initialization in background
|
||||
_adbInitTask = Task.Run(() =>
|
||||
{
|
||||
_ = Logger.Log("Attempting to Initialize ADB Server");
|
||||
if (File.Exists(Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe")))
|
||||
{
|
||||
_ = ADB.RunAdbCommandToString("start-server");
|
||||
}
|
||||
});
|
||||
|
||||
// Basic UI setup
|
||||
CenterToScreen();
|
||||
@@ -454,17 +460,6 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
|
||||
// ADB initialization in background
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
_ = Logger.Log("Attempting to Initialize ADB Server");
|
||||
if (File.Exists(Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe")))
|
||||
{
|
||||
_ = ADB.RunAdbCommandToString("kill-server");
|
||||
_ = ADB.RunAdbCommandToString("start-server");
|
||||
}
|
||||
});
|
||||
|
||||
// Continue with Form1_Shown
|
||||
this.Form1_Shown(sender, e);
|
||||
}
|
||||
@@ -595,9 +590,14 @@ namespace AndroidSideloader
|
||||
changeTitle("Offline mode enabled, no Rclone");
|
||||
}
|
||||
|
||||
changeTitle("Connecting to your Quest...");
|
||||
await Task.Run(() =>
|
||||
// Device connection and Metadata can run simultaneously
|
||||
Task metadataTask = null;
|
||||
Task deviceConnectionTask = null;
|
||||
|
||||
// Start device connection task
|
||||
deviceConnectionTask = Task.Run(() =>
|
||||
{
|
||||
changeTitle("Connecting to device...");
|
||||
if (!string.IsNullOrEmpty(settings.IPAddress))
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "platform-tools", "adb.exe");
|
||||
@@ -645,10 +645,10 @@ namespace AndroidSideloader
|
||||
}
|
||||
});
|
||||
|
||||
// Metadata updates
|
||||
// Start metadata task in parallel
|
||||
if (UsingPublicConfig)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
metadataTask = Task.Run(() =>
|
||||
{
|
||||
changeTitle("Updating Metadata...");
|
||||
SideloaderRCLONE.UpdateMetadataFromPublic();
|
||||
@@ -659,7 +659,7 @@ namespace AndroidSideloader
|
||||
}
|
||||
else if (!isOffline)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
metadataTask = Task.Run(() =>
|
||||
{
|
||||
changeTitle("Updating Game Notes...");
|
||||
SideloaderRCLONE.UpdateGameNotes(currentRemote);
|
||||
@@ -681,6 +681,16 @@ namespace AndroidSideloader
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for both tasks to complete
|
||||
var tasksToWait = new List<Task>();
|
||||
if (deviceConnectionTask != null) tasksToWait.Add(deviceConnectionTask);
|
||||
if (metadataTask != null) tasksToWait.Add(metadataTask);
|
||||
|
||||
if (tasksToWait.Count > 0)
|
||||
{
|
||||
await Task.WhenAll(tasksToWait);
|
||||
}
|
||||
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
changeTitle("Populating Game List...");
|
||||
|
||||
@@ -692,8 +702,7 @@ namespace AndroidSideloader
|
||||
|
||||
// Parallel execution
|
||||
await Task.WhenAll(
|
||||
Task.Run(() => listAppsBtn())//,
|
||||
//Task.Run(() => showAvailableSpace())
|
||||
Task.Run(() => listAppsBtn())
|
||||
);
|
||||
|
||||
downloadInstallGameButton.Enabled = true;
|
||||
@@ -729,7 +738,6 @@ namespace AndroidSideloader
|
||||
}
|
||||
|
||||
changeTitlebarToDevice();
|
||||
//UpdateQuestInfoPanel();
|
||||
}
|
||||
|
||||
private void timer_Tick(object sender, EventArgs e)
|
||||
@@ -789,11 +797,6 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowSubMenu(Panel subMenu)
|
||||
{
|
||||
subMenu.Visible = subMenu.Visible == false;
|
||||
}
|
||||
|
||||
private async void startsideloadbutton_Click(object sender, EventArgs e)
|
||||
{
|
||||
ProcessOutput output = new ProcessOutput("", "");
|
||||
@@ -895,7 +898,7 @@ namespace AndroidSideloader
|
||||
battery = Utilities.StringUtilities.KeepOnlyNumbers(battery);
|
||||
batteryLabel.Text = battery + "%";
|
||||
|
||||
UpdateQuestInfoPanel();
|
||||
//UpdateQuestInfoPanel();
|
||||
|
||||
return devicesComboBox.SelectedIndex;
|
||||
}
|
||||
@@ -1929,21 +1932,20 @@ namespace AndroidSideloader
|
||||
|
||||
private async void initListView(bool favoriteView)
|
||||
{
|
||||
// Stopwatch for perf logging
|
||||
var sw = Stopwatch.StartNew();
|
||||
Logger.Log("initListView started");
|
||||
|
||||
int upToDateCount = 0;
|
||||
int updateAvailableCount = 0;
|
||||
int newerThanListCount = 0;
|
||||
rookienamelist = string.Empty;
|
||||
loaded = false;
|
||||
|
||||
// Read installed packages prepared by listAppsBtn()
|
||||
var rookienameSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var rookienameListBuilder = new StringBuilder();
|
||||
|
||||
string installedApps = settings.InstalledApps;
|
||||
string[] packageList = installedApps.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Early return if no packages available
|
||||
if (packageList.Length == 0)
|
||||
{
|
||||
Logger.Log("No installed packages found");
|
||||
@@ -1963,7 +1965,6 @@ namespace AndroidSideloader
|
||||
blacklist = File.ReadAllLines($"{settings.MainDir}\\nouns\\blacklist.txt");
|
||||
}
|
||||
|
||||
// Merge local blacklist.json if present
|
||||
string localBlacklistPath = Path.Combine(settings.MainDir, "blacklist.json");
|
||||
if (File.Exists(localBlacklistPath))
|
||||
{
|
||||
@@ -1996,40 +1997,41 @@ namespace AndroidSideloader
|
||||
|
||||
Logger.Log($"Blacklist/Whitelist loaded in {sw.ElapsedMilliseconds}ms");
|
||||
|
||||
var GameList = new List<ListViewItem>();
|
||||
var rookieList = new List<string>();
|
||||
var installedGames = packageList.ToList();
|
||||
var blacklistItems = blacklist.ToList();
|
||||
var whitelistItems = whitelist.ToList();
|
||||
int expectedGameCount = SideloaderRCLONE.games.Count > 0 ? SideloaderRCLONE.games.Count : 500;
|
||||
var GameList = new List<ListViewItem>(expectedGameCount);
|
||||
var rookieList = new List<string>(expectedGameCount);
|
||||
|
||||
newGamesToUploadList = whitelistItems.Intersect(installedGames, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var installedGamesSet = new HashSet<string>(packageList, StringComparer.OrdinalIgnoreCase);
|
||||
var blacklistSet = new HashSet<string>(blacklist, StringComparer.OrdinalIgnoreCase);
|
||||
var whitelistSet = new HashSet<string>(whitelist, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
newGamesToUploadList = whitelistSet.Intersect(installedGamesSet, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
if (SideloaderRCLONE.games.Count > 5)
|
||||
{
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
|
||||
// Fast path: collect all installed version codes in one call
|
||||
Dictionary<string, ulong> installedVersions = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
|
||||
// Use full dumpsys to get all version codes at once
|
||||
Dictionary<string, ulong> installedVersions = new Dictionary<string, ulong>(packageList.Length, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Logger.Log("Fast path fetching version codes...");
|
||||
Logger.Log("Fetching version codes via full dumpsys...");
|
||||
var versionSw = Stopwatch.StartNew();
|
||||
|
||||
// Single dumpsys parse
|
||||
try
|
||||
{
|
||||
var dump = ADB.RunAdbCommandToString("shell dumpsys package").Output;
|
||||
var map = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
|
||||
Logger.Log($"Dumpsys returned {dump.Length} chars in {versionSw.ElapsedMilliseconds}ms");
|
||||
versionSw.Restart();
|
||||
|
||||
string currentPkg = null;
|
||||
|
||||
// dumpsys package structure:
|
||||
// Package [com.example.app] (..)
|
||||
// versionCode=12345 ...
|
||||
foreach (var rawLine in dump.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
var line = rawLine.TrimStart();
|
||||
|
||||
if (line.StartsWith("Package [", StringComparison.OrdinalIgnoreCase))
|
||||
if (line.StartsWith("Package [", StringComparison.Ordinal))
|
||||
{
|
||||
var start = line.IndexOf('[');
|
||||
var end = line.IndexOf(']');
|
||||
@@ -2044,42 +2046,42 @@ namespace AndroidSideloader
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentPkg != null && line.StartsWith("versionCode=", StringComparison.OrdinalIgnoreCase))
|
||||
if (currentPkg != null && line.StartsWith("versionCode=", StringComparison.Ordinal))
|
||||
{
|
||||
var after = line.Substring("versionCode=".Length).Trim();
|
||||
var digits = new string(after.TakeWhile(char.IsDigit).ToArray());
|
||||
if (!string.IsNullOrEmpty(digits) && ulong.TryParse(digits, out var v))
|
||||
var after = line.Substring(12);
|
||||
int spaceIdx = after.IndexOf(' ');
|
||||
var digits = spaceIdx > 0 ? after.Substring(0, spaceIdx) : after;
|
||||
if (ulong.TryParse(digits, out var v))
|
||||
{
|
||||
map[currentPkg] = v;
|
||||
// Only store if it's an installed package we care about
|
||||
if (installedGamesSet.Contains(currentPkg))
|
||||
{
|
||||
installedVersions[currentPkg] = v;
|
||||
}
|
||||
}
|
||||
currentPkg = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to the installed third-party packages
|
||||
var set = new HashSet<string>(packageList, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kv in map)
|
||||
{
|
||||
if (set.Contains(kv.Key))
|
||||
{
|
||||
installedVersions[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Collected {installedVersions.Count} version codes in {sw.ElapsedMilliseconds}ms");
|
||||
Logger.Log($"Parsed {installedVersions.Count} version codes in {versionSw.ElapsedMilliseconds}ms");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"'dumpsys package' failed: {ex.Message}", LogLevel.ERROR);
|
||||
}
|
||||
|
||||
Logger.Log($"Version fetch total: {versionSw.ElapsedMilliseconds}ms");
|
||||
});
|
||||
|
||||
// Precompute cloud max version per package to avoid scanning inside the loop
|
||||
var cloudMaxVersionByPackage = new Dictionary<string, ulong>(StringComparer.OrdinalIgnoreCase);
|
||||
Logger.Log($"Version codes collected in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
|
||||
// Precompute cloud max version per package
|
||||
var cloudMaxVersionByPackage = new Dictionary<string, ulong>(SideloaderRCLONE.games.Count, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var release in SideloaderRCLONE.games)
|
||||
{
|
||||
string pkg = release[SideloaderRCLONE.PackageNameIndex];
|
||||
// Parse cloud version once
|
||||
ulong v = 0;
|
||||
try
|
||||
{
|
||||
@@ -2100,37 +2102,32 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Precomputed cloud max versions in {sw.ElapsedMilliseconds}ms");
|
||||
Logger.Log($"Cloud versions precomputed in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
|
||||
// Process games and colorize based on version comparisons
|
||||
// Process games on background thread
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Color colorFont_installedGame = ColorTranslator.FromHtml("#3c91e6");
|
||||
Color colorFont_updateAvailable = ColorTranslator.FromHtml("#4daa57");
|
||||
Color colorFont_donateGame = ColorTranslator.FromHtml("#cb9cf2");
|
||||
Color colorFont_error = ColorTranslator.FromHtml("#f52f57");
|
||||
|
||||
foreach (string[] release in SideloaderRCLONE.games)
|
||||
{
|
||||
string packagename = release[SideloaderRCLONE.PackageNameIndex];
|
||||
rookieList.Add(packagename);
|
||||
|
||||
if (!rookienamelist.Contains(release[SideloaderRCLONE.GameNameIndex]))
|
||||
string gameName = release[SideloaderRCLONE.GameNameIndex];
|
||||
if (rookienameSet.Add(gameName))
|
||||
{
|
||||
rookienamelist += release[SideloaderRCLONE.GameNameIndex] + "\n";
|
||||
rookienameListBuilder.Append(gameName).Append('\n');
|
||||
}
|
||||
|
||||
var item = new ListViewItem(release);
|
||||
|
||||
// Installed?
|
||||
if (installedVersions.TryGetValue(packagename, out ulong installedVersionInt))
|
||||
{
|
||||
item.ForeColor = colorFont_installedGame;
|
||||
item.ForeColor = ColorInstalled;
|
||||
|
||||
try
|
||||
{
|
||||
ulong cloudVersionInt = 0;
|
||||
cloudMaxVersionByPackage.TryGetValue(packagename, out cloudVersionInt);
|
||||
cloudMaxVersionByPackage.TryGetValue(packagename, out ulong cloudVersionInt);
|
||||
|
||||
if (installedVersionInt == cloudVersionInt)
|
||||
{
|
||||
@@ -2138,17 +2135,17 @@ namespace AndroidSideloader
|
||||
}
|
||||
else if (installedVersionInt < cloudVersionInt)
|
||||
{
|
||||
item.ForeColor = colorFont_updateAvailable;
|
||||
item.ForeColor = ColorUpdateAvailable;
|
||||
updateAvailableCount++;
|
||||
}
|
||||
else if (installedVersionInt > cloudVersionInt)
|
||||
{
|
||||
newerThanListCount++;
|
||||
bool dontget = blacklistItems.Contains(packagename, StringComparer.OrdinalIgnoreCase);
|
||||
bool dontget = blacklistSet.Contains(packagename);
|
||||
|
||||
if (!dontget)
|
||||
{
|
||||
item.ForeColor = colorFont_donateGame;
|
||||
item.ForeColor = ColorDonateGame;
|
||||
|
||||
if (!updatesNotified && !isworking && updint < 6 && !settings.SubmittedUpdates.Contains(packagename))
|
||||
{
|
||||
@@ -2163,12 +2160,10 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Logger.Log($"Checked game {release[SideloaderRCLONE.GameNameIndex]}; cloudversion={cloudVersionInt} localversion={installedVersionInt}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
item.ForeColor = colorFont_error;
|
||||
item.ForeColor = ColorError;
|
||||
Logger.Log($"An error occurred while rendering game {release[SideloaderRCLONE.GameNameIndex]} in ListView", LogLevel.ERROR);
|
||||
Logger.Log($"ExMsg: {ex.Message}", LogLevel.ERROR);
|
||||
}
|
||||
@@ -2176,7 +2171,6 @@ namespace AndroidSideloader
|
||||
|
||||
if (favoriteView)
|
||||
{
|
||||
// Only add favorited games when favoriteView is true
|
||||
if (settings.FavoritedGames.Contains(item.SubItems[1].Text))
|
||||
{
|
||||
GameList.Add(item);
|
||||
@@ -2189,11 +2183,13 @@ namespace AndroidSideloader
|
||||
}
|
||||
});
|
||||
|
||||
rookienamelist = rookienameListBuilder.ToString();
|
||||
|
||||
Logger.Log($"Game processing completed in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
}
|
||||
else if (!isOffline)
|
||||
{
|
||||
// If no games are loaded, try switching mirrors then rebuild
|
||||
SwitchMirrors();
|
||||
if (!isOffline)
|
||||
{
|
||||
@@ -2202,22 +2198,22 @@ namespace AndroidSideloader
|
||||
return;
|
||||
}
|
||||
|
||||
if (blacklistItems.Count == 0 && GameList.Count == 0 && !settings.NodeviceMode && !isOffline)
|
||||
if (blacklistSet.Count == 0 && GameList.Count == 0 && !settings.NodeviceMode && !isOffline)
|
||||
{
|
||||
_ = FlexibleMessageBox.Show(Program.form,
|
||||
"Rookie seems to have failed to load all resources. Please try restarting Rookie a few times.\nIf error still persists please disable any VPN or firewalls (rookie uses direct download so a VPN is not needed)\nIf this error still persists try a system reboot, reinstalling the program, and lastly posting the problem on telegram.",
|
||||
"Error loading blacklist or game list!");
|
||||
}
|
||||
|
||||
// New apps detection
|
||||
newGamesList = installedGames
|
||||
.Except(rookieList, StringComparer.OrdinalIgnoreCase)
|
||||
.Except(blacklistItems, StringComparer.OrdinalIgnoreCase)
|
||||
var rookieSet = new HashSet<string>(rookieList, StringComparer.OrdinalIgnoreCase);
|
||||
newGamesList = installedGamesSet
|
||||
.Except(rookieSet, StringComparer.OrdinalIgnoreCase)
|
||||
.Except(blacklistSet, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (blacklistItems.Count > 100 && rookieList.Count > 100)
|
||||
if (blacklistSet.Count > 100 && rookieList.Count > 100)
|
||||
{
|
||||
await ProcessNewApps(newGamesList, blacklistItems);
|
||||
await ProcessNewApps(newGamesList, blacklistSet.ToList());
|
||||
}
|
||||
|
||||
progressBar.Style = ProgressBarStyle.Continuous;
|
||||
@@ -2235,11 +2231,11 @@ namespace AndroidSideloader
|
||||
{
|
||||
changeTitle("Populating update list...\n\n");
|
||||
lblUpToDate.Text = $"[{upToDateCount}] INSTALLED";
|
||||
lblUpToDate.ForeColor = ColorTranslator.FromHtml("#3c91e6");
|
||||
lblUpToDate.ForeColor = ColorInstalled;
|
||||
lblUpdateAvailable.Text = $"[{updateAvailableCount}] UPDATE AVAILABLE";
|
||||
lblUpdateAvailable.ForeColor = ColorTranslator.FromHtml("#4daa57");
|
||||
lblUpdateAvailable.ForeColor = ColorUpdateAvailable;
|
||||
lblNeedsDonate.Text = $"[{newerThanListCount}] NEWER THAN LIST";
|
||||
lblNeedsDonate.ForeColor = ColorTranslator.FromHtml("#cb9cf2");
|
||||
lblNeedsDonate.ForeColor = ColorDonateGame;
|
||||
|
||||
ListViewItem[] arr = GameList.ToArray();
|
||||
gamesListView.BeginUpdate();
|
||||
@@ -2248,32 +2244,36 @@ namespace AndroidSideloader
|
||||
gamesListView.EndUpdate();
|
||||
});
|
||||
|
||||
Logger.Log($"UI updated in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
|
||||
changeTitle("\n\n");
|
||||
|
||||
// Build search index only once
|
||||
if (!_allItemsInitialized)
|
||||
{
|
||||
_allItems = gamesListView.Items.Cast<ListViewItem>().ToList();
|
||||
|
||||
_searchIndex = new Dictionary<string, List<ListViewItem>>(StringComparer.OrdinalIgnoreCase);
|
||||
_searchIndex = new Dictionary<string, List<ListViewItem>>(_allItems.Count * 2, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in _allItems)
|
||||
{
|
||||
string gameName = item.Text;
|
||||
if (!_searchIndex.ContainsKey(gameName))
|
||||
string gameNameKey = item.Text;
|
||||
if (!_searchIndex.TryGetValue(gameNameKey, out var list))
|
||||
{
|
||||
_searchIndex[gameName] = new List<ListViewItem>();
|
||||
list = new List<ListViewItem>(1);
|
||||
_searchIndex[gameNameKey] = list;
|
||||
}
|
||||
_searchIndex[gameName].Add(item);
|
||||
list.Add(item);
|
||||
|
||||
if (item.SubItems.Count > 1)
|
||||
{
|
||||
string releaseName = item.SubItems[1].Text;
|
||||
if (!_searchIndex.ContainsKey(releaseName))
|
||||
if (!_searchIndex.TryGetValue(releaseName, out var releaseList))
|
||||
{
|
||||
_searchIndex[releaseName] = new List<ListViewItem>();
|
||||
releaseList = new List<ListViewItem>(1);
|
||||
_searchIndex[releaseName] = releaseList;
|
||||
}
|
||||
_searchIndex[releaseName].Add(item);
|
||||
releaseList.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2281,12 +2281,11 @@ namespace AndroidSideloader
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
Logger.Log($"initListView completed in {sw.ElapsedMilliseconds}ms");
|
||||
Logger.Log($"initListView total completed in {sw.ElapsedMilliseconds}ms");
|
||||
|
||||
// If gallery view is active, refresh it with the newly loaded data
|
||||
if (isGalleryView && gamesGalleryView.Visible)
|
||||
{
|
||||
_galleryDataSource = null; // Reset so PopulateGalleryView uses fresh _allItems
|
||||
_galleryDataSource = null;
|
||||
PopulateGalleryView();
|
||||
}
|
||||
}
|
||||
@@ -5148,7 +5147,6 @@ function onYouTubeIframeAPIReady() {
|
||||
long usedSpace = 0;
|
||||
long freeSpace = 0;
|
||||
|
||||
|
||||
if (lines.Length > 1)
|
||||
{
|
||||
string[] parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AndroidSideloader.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
@@ -67,66 +68,90 @@ namespace AndroidSideloader
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = Logger.Log($"Extracting Metadata");
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
// Cache commonly used paths to avoid repeated Path.Combine calls
|
||||
string currentDir = Environment.CurrentDirectory;
|
||||
string metaRoot = Path.Combine(currentDir, "meta");
|
||||
string metaArchive = Path.Combine(currentDir, "meta.7z");
|
||||
string metaDotMeta = Path.Combine(metaRoot, ".meta");
|
||||
|
||||
Zip.ExtractFile(metaArchive, metaRoot, MainForm.PublicConfigFile.Password);
|
||||
// Check if archive exists and is newer than existing metadata
|
||||
if (!File.Exists(metaArchive))
|
||||
{
|
||||
Logger.Log("meta.7z not found, skipping extraction", LogLevel.WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Logger.Log($"Updating Metadata");
|
||||
|
||||
// Use a fast directory reset: delete if exists, then move (avoids partial state)
|
||||
SafeDeleteDirectory(Nouns);
|
||||
SafeDeleteDirectory(ThumbnailsFolder);
|
||||
SafeDeleteDirectory(NotesFolder);
|
||||
|
||||
// Avoid throwing if source folders are missing
|
||||
MoveIfExists(Path.Combine(metaDotMeta, "nouns"), Nouns);
|
||||
MoveIfExists(Path.Combine(metaDotMeta, "thumbnails"), ThumbnailsFolder);
|
||||
MoveIfExists(Path.Combine(metaDotMeta, "notes"), NotesFolder);
|
||||
|
||||
_ = Logger.Log($"Initializing Games List");
|
||||
|
||||
// Stream the file line-by-line instead of reading the whole file into memory
|
||||
// Skip extraction if metadata is already up-to-date (based on file timestamps)
|
||||
string gameListPath = Path.Combine(metaRoot, "VRP-GameList.txt");
|
||||
if (File.Exists(gameListPath))
|
||||
{
|
||||
games.Clear();
|
||||
bool isFirstLine = true;
|
||||
foreach (var line in File.ReadLines(gameListPath))
|
||||
var archiveTime = File.GetLastWriteTimeUtc(metaArchive);
|
||||
var gameListTime = File.GetLastWriteTimeUtc(gameListPath);
|
||||
|
||||
// If game list is newer than archive, skip extraction
|
||||
if (gameListTime > archiveTime && games.Count > 0)
|
||||
{
|
||||
// Skip header line only once
|
||||
if (isFirstLine)
|
||||
{
|
||||
isFirstLine = false;
|
||||
continue;
|
||||
}
|
||||
Logger.Log($"Metadata already up-to-date, skipping extraction");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip empty/whitespace lines without allocating split arrays
|
||||
_ = Logger.Log($"Extracting Metadata");
|
||||
Zip.ExtractFile(metaArchive, metaRoot, MainForm.PublicConfigFile.Password);
|
||||
Logger.Log($"Extraction completed in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
|
||||
_ = Logger.Log($"Updating Metadata");
|
||||
|
||||
// Use Parallel.Invoke for independent directory operations
|
||||
System.Threading.Tasks.Parallel.Invoke(
|
||||
() => SafeDeleteDirectory(Nouns),
|
||||
() => SafeDeleteDirectory(ThumbnailsFolder),
|
||||
() => SafeDeleteDirectory(NotesFolder)
|
||||
);
|
||||
Logger.Log($"Directory cleanup in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
|
||||
// Move directories
|
||||
MoveIfExists(Path.Combine(metaDotMeta, "nouns"), Nouns);
|
||||
MoveIfExists(Path.Combine(metaDotMeta, "thumbnails"), ThumbnailsFolder);
|
||||
MoveIfExists(Path.Combine(metaDotMeta, "notes"), NotesFolder);
|
||||
Logger.Log($"Directory moves in {sw.ElapsedMilliseconds}ms");
|
||||
sw.Restart();
|
||||
|
||||
_ = Logger.Log($"Initializing Games List");
|
||||
|
||||
gameListPath = Path.Combine(metaRoot, "VRP-GameList.txt");
|
||||
if (File.Exists(gameListPath))
|
||||
{
|
||||
// Read all lines at once - faster for files that fit in memory
|
||||
var lines = File.ReadAllLines(gameListPath);
|
||||
var newGames = new List<string[]>(lines.Length);
|
||||
|
||||
for (int i = 1; i < lines.Length; i++) // Skip header
|
||||
{
|
||||
var line = lines[i];
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split with RemoveEmptyEntries to avoid trailing empty fields
|
||||
var splitGame = line.Split(new[] { ';' }, StringSplitOptions.None);
|
||||
// Minimal validation: require at least 2 fields
|
||||
var splitGame = line.Split(';');
|
||||
if (splitGame.Length > 1)
|
||||
{
|
||||
games.Add(splitGame);
|
||||
newGames.Add(splitGame);
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic swap
|
||||
games.Clear();
|
||||
games.AddRange(newGames);
|
||||
Logger.Log($"Parsed {games.Count} games in {sw.ElapsedMilliseconds}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = Logger.Log("VRP-GameList.txt not found in extracted metadata.", LogLevel.WARNING);
|
||||
}
|
||||
|
||||
// Delete meta folder at the end to avoid leaving partial state if something fails earlier
|
||||
SafeDeleteDirectory(metaRoot);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -136,42 +161,6 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
|
||||
public static void RefreshRemotes()
|
||||
{
|
||||
_ = Logger.Log($"Refresh / List Remotes");
|
||||
RemotesList.Clear();
|
||||
|
||||
// Avoid unnecessary ToArray; directly iterate lines
|
||||
var output = RCLONE.runRcloneCommand_DownloadConfig("listremotes").Output;
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
_ = Logger.Log("No remotes returned from rclone.");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Logger.Log("Loaded following remotes: ");
|
||||
foreach (var r in SplitLines(output))
|
||||
{
|
||||
if (r.Length <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trim whitespace and trailing colon if present
|
||||
var remote = r.TrimEnd();
|
||||
if (remote.EndsWith(":"))
|
||||
{
|
||||
remote = remote.Substring(0, remote.Length - 1);
|
||||
}
|
||||
|
||||
if (remote.IndexOf("mirror", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
_ = Logger.Log(remote);
|
||||
RemotesList.Add(remote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void initGames(string remote)
|
||||
{
|
||||
_ = Logger.Log($"Initializing Games List");
|
||||
@@ -248,13 +237,65 @@ namespace AndroidSideloader
|
||||
}
|
||||
}
|
||||
|
||||
// Robust directory delete without throwing if not present
|
||||
// Fast directory delete using Windows cmd - faster than .NET's Directory.Delete
|
||||
// for large directories with many files (e.g., thumbnails folder with 1000+ images)
|
||||
private static void SafeDeleteDirectory(string path)
|
||||
{
|
||||
// Avoid exceptions when directory is missing
|
||||
if (Directory.Exists(path))
|
||||
if (!Directory.Exists(path))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
// Use Windows rd command which is ~10x faster than .NET's recursive delete
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c rd /s /q \"{path}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
using (var process = Process.Start(psi))
|
||||
{
|
||||
// Wait with timeout to prevent hanging
|
||||
if (!process.WaitForExit(30000)) // 30 second timeout
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
Logger.Log($"Directory delete timed out for: {path}", LogLevel.WARNING);
|
||||
// Fallback to .NET delete
|
||||
FallbackDelete(path);
|
||||
}
|
||||
else if (process.ExitCode != 0 && Directory.Exists(path))
|
||||
{
|
||||
// Command failed, try fallback
|
||||
FallbackDelete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Fast delete failed for {path}: {ex.Message}", LogLevel.WARNING);
|
||||
// Fallback to standard .NET delete
|
||||
FallbackDelete(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback delete method using standard .NET
|
||||
private static void FallbackDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Fallback delete also failed for {path}: {ex.Message}", LogLevel.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +305,8 @@ namespace AndroidSideloader
|
||||
if (Directory.Exists(sourceDir))
|
||||
{
|
||||
// Ensure destination does not exist to prevent IOException
|
||||
if (Directory.Exists(destDir))
|
||||
{
|
||||
Directory.Delete(destDir, true);
|
||||
}
|
||||
// Use fast delete method
|
||||
SafeDeleteDirectory(destDir);
|
||||
Directory.Move(sourceDir, destDir);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -51,8 +51,9 @@ namespace AndroidSideloader
|
||||
settings.CurrentLogPath = logFilePath;
|
||||
settings.Save();
|
||||
|
||||
// Initial log entry
|
||||
Log($"Logger initialized at: {DateTime.Now:hh:mmtt(UTC)}", LogLevel.INFO);
|
||||
// Initial log entry, make it stand out
|
||||
string time = DateTime.UtcNow.ToString("hh:mm:ss.fff tt (UTC): ");
|
||||
Log($"\n\n{time}------------ Logger initialized ------------", LogLevel.INFO);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user