UI: Multithreaded Updater (#2031)
* Use multiple threads to download different chunks of an update simultaneously. This reduces time to complete the download significantly. * Remove dirty-flag check (for test purposes) * Clean up updater code. * Include fallback to single-threaded updater if mt fails * Reduce connection count to 4. * Improve fallback on error. Correct issue where data was missing during download due to total build size not being cleanly divisble by the connection count. Cleaned up unnecessary code. * Add missing return statements * Fix alignment * Alignment * More alignment * Rely on content-range request instead of xml/json size property. * Re-instate dirty checking and version checking to move into review stage. * Address comments * Address comments * Comments * Comments * Final...? * final final * final final final nit * Use Array.Copy as requested by rip * Updated some names for clarity. * Move addition into for loop (to shorten line width) * Add missing semicolon -- forgot to stage :9
This commit is contained in:
parent
9bda7b4699
commit
4e26aed816
2 changed files with 173 additions and 7 deletions
|
@ -73,7 +73,7 @@ namespace Ryujinx.Modules
|
||||||
SecondaryText.Text = "";
|
SecondaryText.Text = "";
|
||||||
_restartQuery = true;
|
_restartQuery = true;
|
||||||
|
|
||||||
_ = Updater.UpdateRyujinx(this, _buildUrl);
|
Updater.UpdateRyujinx(this, _buildUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,13 @@ using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Ui;
|
using Ryujinx.Ui;
|
||||||
using Ryujinx.Ui.Widgets;
|
using Ryujinx.Ui.Widgets;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Ryujinx.Modules
|
namespace Ryujinx.Modules
|
||||||
|
@ -23,11 +25,13 @@ namespace Ryujinx.Modules
|
||||||
private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
|
private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||||
private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish");
|
private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish");
|
||||||
|
private static readonly int ConnectionCount = 4;
|
||||||
|
|
||||||
private static string _jobId;
|
private static string _jobId;
|
||||||
private static string _buildVer;
|
private static string _buildVer;
|
||||||
private static string _platformExt;
|
private static string _platformExt;
|
||||||
private static string _buildUrl;
|
private static string _buildUrl;
|
||||||
|
private static long _buildSize;
|
||||||
|
|
||||||
private const string AppveyorApiUrl = "https://ci.appveyor.com/api";
|
private const string AppveyorApiUrl = "https://ci.appveyor.com/api";
|
||||||
|
|
||||||
|
@ -38,18 +42,30 @@ namespace Ryujinx.Modules
|
||||||
Running = true;
|
Running = true;
|
||||||
mainWindow.UpdateMenuItem.Sensitive = false;
|
mainWindow.UpdateMenuItem.Sensitive = false;
|
||||||
|
|
||||||
|
int artifactIndex = -1;
|
||||||
|
|
||||||
// Detect current platform
|
// Detect current platform
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
{
|
{
|
||||||
_platformExt = "osx_x64.zip";
|
_platformExt = "osx_x64.zip";
|
||||||
|
artifactIndex = 1;
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
_platformExt = "win_x64.zip";
|
_platformExt = "win_x64.zip";
|
||||||
|
artifactIndex = 2;
|
||||||
}
|
}
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
_platformExt = "linux_x64.tar.gz";
|
_platformExt = "linux_x64.tar.gz";
|
||||||
|
artifactIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artifactIndex == -1)
|
||||||
|
{
|
||||||
|
GtkDialog.CreateErrorDialog("Your platform is not supported!");
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Version newVersion;
|
Version newVersion;
|
||||||
|
@ -72,6 +88,7 @@ namespace Ryujinx.Modules
|
||||||
{
|
{
|
||||||
using (WebClient jsonClient = new WebClient())
|
using (WebClient jsonClient = new WebClient())
|
||||||
{
|
{
|
||||||
|
// Fetch latest build information
|
||||||
string fetchedJson = await jsonClient.DownloadStringTaskAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master");
|
string fetchedJson = await jsonClient.DownloadStringTaskAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master");
|
||||||
JObject jsonRoot = JObject.Parse(fetchedJson);
|
JObject jsonRoot = JObject.Parse(fetchedJson);
|
||||||
JToken buildToken = jsonRoot["build"];
|
JToken buildToken = jsonRoot["build"];
|
||||||
|
@ -125,12 +142,32 @@ namespace Ryujinx.Modules
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch build size information to learn chunk sizes.
|
||||||
|
using (WebClient buildSizeClient = new WebClient())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
buildSizeClient.Headers.Add("Range", "bytes=0-0");
|
||||||
|
await buildSizeClient.DownloadDataTaskAsync(new Uri(_buildUrl));
|
||||||
|
|
||||||
|
string contentRange = buildSizeClient.ResponseHeaders["Content-Range"];
|
||||||
|
_buildSize = long.Parse(contentRange.Substring(contentRange.IndexOf('/') + 1));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||||
|
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, will use single-threaded updater");
|
||||||
|
|
||||||
|
_buildSize = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show a message asking the user if they want to update
|
// Show a message asking the user if they want to update
|
||||||
UpdateDialog updateDialog = new UpdateDialog(mainWindow, newVersion, _buildUrl);
|
UpdateDialog updateDialog = new UpdateDialog(mainWindow, newVersion, _buildUrl);
|
||||||
updateDialog.Show();
|
updateDialog.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
|
public static void UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
|
||||||
{
|
{
|
||||||
// Empty update dir, although it shouldn't ever have anything inside it
|
// Empty update dir, although it shouldn't ever have anything inside it
|
||||||
if (Directory.Exists(UpdateDir))
|
if (Directory.Exists(UpdateDir))
|
||||||
|
@ -147,6 +184,126 @@ namespace Ryujinx.Modules
|
||||||
updateDialog.ProgressBar.Value = 0;
|
updateDialog.ProgressBar.Value = 0;
|
||||||
updateDialog.ProgressBar.MaxValue = 100;
|
updateDialog.ProgressBar.MaxValue = 100;
|
||||||
|
|
||||||
|
if (_buildSize >= 0)
|
||||||
|
{
|
||||||
|
DoUpdateWithMultipleThreads(updateDialog, downloadUrl, updateFile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DoUpdateWithMultipleThreads(UpdateDialog updateDialog, string downloadUrl, string updateFile)
|
||||||
|
{
|
||||||
|
// Multi-Threaded Updater
|
||||||
|
long chunkSize = _buildSize / ConnectionCount;
|
||||||
|
long remainderChunk = _buildSize % ConnectionCount;
|
||||||
|
|
||||||
|
int completedRequests = 0;
|
||||||
|
int totalProgressPercentage = 0;
|
||||||
|
int[] progressPercentage = new int[ConnectionCount];
|
||||||
|
|
||||||
|
List<byte[]> list = new List<byte[]>(ConnectionCount);
|
||||||
|
List<WebClient> webClients = new List<WebClient>(ConnectionCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < ConnectionCount; i++)
|
||||||
|
{
|
||||||
|
list.Add(new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < ConnectionCount; i++)
|
||||||
|
{
|
||||||
|
using (WebClient client = new WebClient())
|
||||||
|
{
|
||||||
|
webClients.Add(client);
|
||||||
|
|
||||||
|
if (i == ConnectionCount - 1)
|
||||||
|
{
|
||||||
|
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
|
||||||
|
}
|
||||||
|
|
||||||
|
client.DownloadProgressChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
int index = (int)args.UserState;
|
||||||
|
|
||||||
|
Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
|
||||||
|
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
|
||||||
|
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
|
||||||
|
|
||||||
|
updateDialog.ProgressBar.Value = totalProgressPercentage / ConnectionCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
client.DownloadDataCompleted += (_, args) =>
|
||||||
|
{
|
||||||
|
int index = (int)args.UserState;
|
||||||
|
|
||||||
|
if (args.Cancelled)
|
||||||
|
{
|
||||||
|
webClients[index].Dispose();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list[index] = args.Result;
|
||||||
|
Interlocked.Increment(ref completedRequests);
|
||||||
|
|
||||||
|
if (Interlocked.Equals(completedRequests, ConnectionCount))
|
||||||
|
{
|
||||||
|
byte[] mergedFileBytes = new byte[_buildSize];
|
||||||
|
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
|
||||||
|
{
|
||||||
|
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
|
||||||
|
destinationOffset += list[connectionIndex].Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllBytes(updateFile, mergedFileBytes);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InstallUpdate(updateDialog, updateFile);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, e.Message);
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||||
|
|
||||||
|
DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
client.DownloadDataAsync(new Uri(downloadUrl), i);
|
||||||
|
}
|
||||||
|
catch (WebException ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||||
|
|
||||||
|
for (int j = 0; j < webClients.Count; j++)
|
||||||
|
{
|
||||||
|
webClients[j].CancelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
|
||||||
|
{
|
||||||
|
// Single-Threaded Updater
|
||||||
using (WebClient client = new WebClient())
|
using (WebClient client = new WebClient())
|
||||||
{
|
{
|
||||||
client.DownloadProgressChanged += (_, args) =>
|
client.DownloadProgressChanged += (_, args) =>
|
||||||
|
@ -154,9 +311,18 @@ namespace Ryujinx.Modules
|
||||||
updateDialog.ProgressBar.Value = args.ProgressPercentage;
|
updateDialog.ProgressBar.Value = args.ProgressPercentage;
|
||||||
};
|
};
|
||||||
|
|
||||||
await client.DownloadFileTaskAsync(downloadUrl, updateFile);
|
client.DownloadDataCompleted += (_, args) =>
|
||||||
|
{
|
||||||
|
File.WriteAllBytes(updateFile, args.Result);
|
||||||
|
InstallUpdate(updateDialog, updateFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.DownloadDataAsync(new Uri(downloadUrl));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async void InstallUpdate(UpdateDialog updateDialog, string updateFile)
|
||||||
|
{
|
||||||
// Extract Update
|
// Extract Update
|
||||||
updateDialog.MainText.Text = "Extracting Update...";
|
updateDialog.MainText.Text = "Extracting Update...";
|
||||||
updateDialog.ProgressBar.Value = 0;
|
updateDialog.ProgressBar.Value = 0;
|
||||||
|
|
Reference in a new issue