add UI for Mare Data Analysis

This commit is contained in:
rootdarkarchon
2023-07-12 15:03:15 +02:00
parent 405c0968da
commit 42da1855b1
12 changed files with 474 additions and 98 deletions

View File

@@ -4,18 +4,22 @@ namespace MareSynchronos.FileCache;
public class FileCacheEntity public class FileCacheEntity
{ {
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks) public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null)
{ {
Size = size;
CompressedSize = compressedSize;
Hash = hash; Hash = hash;
PrefixedFilePath = path; PrefixedFilePath = path;
LastModifiedDateTicks = lastModifiedDateTicks; LastModifiedDateTicks = lastModifiedDateTicks;
} }
public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}"; public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}|{Size ?? -1}|{CompressedSize ?? -1}";
public string Hash { get; set; } public string Hash { get; set; }
public string LastModifiedDateTicks { get; set; } public string LastModifiedDateTicks { get; set; }
public string PrefixedFilePath { get; init; } public string PrefixedFilePath { get; init; }
public string ResolvedFilepath { get; private set; } = string.Empty; public string ResolvedFilepath { get; private set; } = string.Empty;
public long? Size { get; set; }
public long? CompressedSize { get; set; }
public void SetResolvedFilePath(string filePath) public void SetResolvedFilePath(string filePath)
{ {

View File

@@ -64,7 +64,20 @@ public sealed class FileCacheManager : IDisposable
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length); if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
var path = splittedEntry[1]; var path = splittedEntry[1];
var time = splittedEntry[2]; var time = splittedEntry[2];
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time))); long size = -1;
long compressed = -1;
if (splittedEntry.Length > 3)
{
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
{
size = result;
}
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
{
compressed = resultCompressed;
}
}
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -122,6 +135,21 @@ public sealed class FileCacheManager : IDisposable
return null; return null;
} }
public List<FileCacheEntity> GetAllFileCachesByHash(string hash)
{
List<FileCacheEntity> output = new();
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
{
foreach (var filecache in fileCacheEntities)
{
var validated = GetValidatedFileCache(filecache);
if (validated != null) output.Add(validated);
}
}
return output;
}
public FileCacheEntity? GetFileCacheByPath(string path) public FileCacheEntity? GetFileCacheByPath(string path)
{ {
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
@@ -158,11 +186,17 @@ public sealed class FileCacheManager : IDisposable
} }
} }
public void UpdateHashedFile(FileCacheEntity fileCache) public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
{ {
_logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath); _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); if (computeProperties)
fileCache.LastModifiedDateTicks = new FileInfo(fileCache.ResolvedFilepath).LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); {
var fi = new FileInfo(fileCache.ResolvedFilepath);
fileCache.Size = fi.Length;
fileCache.CompressedSize = null;
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
}
RemoveHashedFile(fileCache); RemoveHashedFile(fileCache);
AddHashedFile(fileCache); AddHashedFile(fileCache);
} }
@@ -247,7 +281,7 @@ public sealed class FileCacheManager : IDisposable
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
{ {
hash ??= Crypto.GetFileHash(fileInfo.FullName); hash ??= Crypto.GetFileHash(fileInfo.FullName);
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)); var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length);
entity = ReplacePathPrefixes(entity); entity = ReplacePathPrefixes(entity);
AddHashedFile(entity); AddHashedFile(entity);
lock (_fileWriteLock) lock (_fileWriteLock)

View File

@@ -87,6 +87,10 @@
<HintPath>$(DalamudLibPath)CheapLoc.dll</HintPath> <HintPath>$(DalamudLibPath)CheapLoc.dll</HintPath>
<Private>false</Private> <Private>false</Private>
</Reference> </Reference>
<Reference Include="Dalamud.Interface">
<HintPath>$(DalamudLibPath)Dalamud.Interface.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -106,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>(); collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>(); collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>(); collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(), collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
s.GetRequiredService<MareMediator>(), s.GetRequiredService<ApiController>(), pluginInterface.UiBuilder, s.GetRequiredService<UiSharedService>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<ApiController>(), pluginInterface.UiBuilder, s.GetRequiredService<UiSharedService>(),
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareProfileManager>())); s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareProfileManager>()));
@@ -118,7 +119,7 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareMediator>())); s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareMediator>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<UiService>(), collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<UiService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<PeriodicFileScanner>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<MareMediator>(), s.GetRequiredService<CharacterAnalyzer>())); s.GetRequiredService<MareMediator>()));
collection.AddScoped((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(), collection.AddScoped((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<MareMediator>(), pluginInterface.UiBuilder, chatGui, s.GetRequiredService<MareConfigService>())); s.GetRequiredService<MareMediator>(), pluginInterface.UiBuilder, chatGui, s.GetRequiredService<MareConfigService>()));
collection.AddScoped((s) => new UiSharedService(s.GetRequiredService<ILogger<UiSharedService>>(), s.GetRequiredService<IpcManager>(), s.GetRequiredService<ApiController>(), collection.AddScoped((s) => new UiSharedService(s.GetRequiredService<ILogger<UiSharedService>>(), s.GetRequiredService<IpcManager>(), s.GetRequiredService<ApiController>(),

View File

@@ -1,4 +1,5 @@
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.UI; using MareSynchronos.UI;
@@ -10,82 +11,146 @@ namespace MareSynchronos.Services;
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private CharacterData? _lastCreatedData;
private CancellationTokenSource? _analysisCts; private CancellationTokenSource? _analysisCts;
private string _lastDataHash = string.Empty;
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = new();
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager) : base(logger, mediator) public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager) : base(logger, mediator)
{ {
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) => Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
{ {
_lastCreatedData = msg.CharacterData.DeepClone(); _ = Task.Run(() => BaseAnalysis(msg.CharacterData.DeepClone()));
}); });
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
} }
public bool IsAnalysisRunning => _analysisCts != null; public bool IsAnalysisRunning => _analysisCts != null;
public int CurrentFile { get; internal set; }
public int TotalFiles { get; internal set; }
public void CancelAnalyze() public void CancelAnalyze()
{ {
_analysisCts?.CancelDispose(); _analysisCts?.CancelDispose();
_analysisCts = null; _analysisCts = null;
} }
public async Task Analyze() public async Task ComputeAnalysis(bool print = true)
{ {
Logger.LogDebug("=== Calculating Character Analysis ===");
_analysisCts = _analysisCts?.CancelRecreate() ?? new(); _analysisCts = _analysisCts?.CancelRecreate() ?? new();
var cancelToken = _analysisCts.Token; var cancelToken = _analysisCts.Token;
if (_lastCreatedData == null) return; var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
if (allFiles.Exists(c => !c.IsComputed))
Logger.LogInformation("=== Calculating Character Analysis, this may take a while ===");
foreach (var obj in _lastCreatedData.FileReplacements)
{ {
Logger.LogInformation("=== File Calculation for {obj} ===", obj.Key); var remaining = allFiles.Where(c => !c.IsComputed).ToList();
Dictionary<string, List<DataEntry>> data = new(StringComparer.OrdinalIgnoreCase); TotalFiles = remaining.Count;
var totalFiles = obj.Value.Count(c => !string.IsNullOrEmpty(c.Hash)); CurrentFile = 1;
var currentFile = 1; Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count);
foreach (var hash in obj.Value.Select(c => c.Hash))
Mediator.Publish(new HaltScanMessage("CharacterAnalyzer"));
foreach (var file in remaining)
{ {
var fileCacheEntry = _fileCacheManager.GetFileCacheByHash(hash); await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false);
if (fileCacheEntry == null) continue; CurrentFile++;
Logger.LogInformation("Computing File {x}/{y}: {hash}", currentFile, totalFiles, hash);
Logger.LogInformation(" File Path: {path}", fileCacheEntry.ResolvedFilepath);
var filePath = fileCacheEntry.ResolvedFilepath;
FileInfo fi = new(filePath);
var ext = fi.Extension;
if (!data.ContainsKey(ext)) data[ext] = new List<DataEntry>();
(_, byte[] fileLength) = await _fileCacheManager.GetCompressedFileData(hash, cancelToken).ConfigureAwait(false);
Logger.LogInformation(" Original Size: {size}, Compressed Size: {compr}",
UiSharedService.ByteToString(fi.Length), UiSharedService.ByteToString(fileLength.LongLength));
data[ext].Add(new DataEntry(fi.FullName, fi.Length, fileLength.LongLength));
currentFile++;
cancelToken.ThrowIfCancellationRequested();
} }
Logger.LogInformation("=== Summary by file type for {obj} ===", obj.Key); _fileCacheManager.WriteOutFullCsv();
foreach (var entry in data)
{
Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Value.Count,
UiSharedService.ByteToString(entry.Value.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Value.Sum(v => v.CompressedSize)));
}
Logger.LogInformation("=== Total summary for {obj} ===", obj.Key);
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", data.Values.Sum(c => c.Count),
UiSharedService.ByteToString(data.Values.Sum(v => v.Sum(c => c.OriginalSize))), UiSharedService.ByteToString(data.Values.Sum(v => v.Sum(c => c.CompressedSize))));
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Mare up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly."); Mediator.Publish(new ResumeScanMessage("CharacterAnalzyer"));
} }
Mediator.Publish(new CharacterDataAnalyzedMessage());
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
_analysisCts = null; _analysisCts = null;
if (print) PrintAnalysis();
}
private void BaseAnalysis(CharacterData charaData)
{
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
LastAnalysis.Clear();
foreach (var obj in charaData.FileReplacements)
{
Dictionary<string, FileDataEntry> data = new(StringComparer.OrdinalIgnoreCase);
foreach (var fileEntry in obj.Value)
{
var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash);
if (fileCacheEntries.Count == 0) continue;
var filePath = fileCacheEntries[0].ResolvedFilepath;
FileInfo fi = new(filePath);
var ext = fi.Extension[1..];
foreach (var entry in fileCacheEntries)
{
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
fileEntry.GamePaths.ToList(),
fileCacheEntries.Select(c => c.ResolvedFilepath).ToList(),
entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0);
}
}
LastAnalysis[obj.Key] = data;
}
Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value;
}
private void PrintAnalysis()
{
if (LastAnalysis.Count == 0) return;
foreach (var kvp in LastAnalysis)
{
int fileCounter = 1;
int totalFiles = kvp.Value.Count;
Logger.LogInformation("=== Analysis for {obj} ===", kvp.Key);
foreach (var entry in kvp.Value.OrderBy(b => b.Value.GamePaths.OrderBy(p => p, StringComparer.Ordinal).First(), StringComparer.Ordinal))
{
Logger.LogInformation("File {x}/{y}: {hash}", fileCounter++, totalFiles, entry.Key);
foreach (var path in entry.Value.GamePaths)
{
Logger.LogInformation(" Game Path: {path}", path);
}
if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected", entry.Key);
foreach (var filePath in entry.Value.FilePaths)
{
Logger.LogInformation(" File Path: {path}", filePath);
}
Logger.LogInformation(" Size: {size}, Compressed: {compressed}", UiSharedService.ByteToString(entry.Value.OriginalSize),
UiSharedService.ByteToString(entry.Value.CompressedSize));
}
}
foreach (var kvp in LastAnalysis)
{
Logger.LogInformation("=== Detailed summary by file type for {obj} ===", kvp.Key);
foreach (var entry in kvp.Value.Select(v => v.Value).GroupBy(v => v.FileType, StringComparer.Ordinal))
{
Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Count(),
UiSharedService.ByteToString(entry.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Sum(v => v.CompressedSize)));
}
Logger.LogInformation("=== Total summary for {obj} ===", kvp.Key);
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", kvp.Value.Count,
UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.OriginalSize)), UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.CompressedSize)));
}
Logger.LogInformation("=== Total summary for all currently present objects ===");
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}",
LastAnalysis.Values.Sum(v => v.Values.Count),
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))),
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Mare up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
} }
public void Dispose() public void Dispose()
@@ -93,5 +158,23 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
} }
private sealed record DataEntry(string filePath, long OriginalSize, long CompressedSize); internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize)
{
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
{
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
var normalSize = new FileInfo(FilePaths[0]).Length;
var entries = fileCacheManager.GetAllFileCachesByHash(Hash);
foreach (var entry in entries)
{
entry.Size = normalSize;
entry.CompressedSize = compressedsize.Item2.LongLength;
}
OriginalSize = normalSize;
CompressedSize = compressedsize.Item2.LongLength;
}
public long OriginalSize { get; private set; } = OriginalSize;
public long CompressedSize { get; private set; } = CompressedSize;
}
} }

View File

@@ -14,7 +14,6 @@ public sealed class CommandManagerService : IDisposable
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly CommandManager _commandManager; private readonly CommandManager _commandManager;
private readonly MareMediator _mediator; private readonly MareMediator _mediator;
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly PerformanceCollectorService _performanceCollectorService; private readonly PerformanceCollectorService _performanceCollectorService;
private readonly PeriodicFileScanner _periodicFileScanner; private readonly PeriodicFileScanner _periodicFileScanner;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
@@ -22,7 +21,7 @@ public sealed class CommandManagerService : IDisposable
public CommandManagerService(CommandManager commandManager, PerformanceCollectorService performanceCollectorService, public CommandManagerService(CommandManager commandManager, PerformanceCollectorService performanceCollectorService,
UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner, UiService uiService, ServerConfigurationManager serverConfigurationManager, PeriodicFileScanner periodicFileScanner,
ApiController apiController, MareMediator mediator, CharacterAnalyzer characterAnalyzer) ApiController apiController, MareMediator mediator)
{ {
_commandManager = commandManager; _commandManager = commandManager;
_performanceCollectorService = performanceCollectorService; _performanceCollectorService = performanceCollectorService;
@@ -31,7 +30,6 @@ public sealed class CommandManagerService : IDisposable
_periodicFileScanner = periodicFileScanner; _periodicFileScanner = periodicFileScanner;
_apiController = apiController; _apiController = apiController;
_mediator = mediator; _mediator = mediator;
_characterAnalyzer = characterAnalyzer;
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand) _commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
{ {
HelpMessage = "Opens the Mare Synchronos UI" HelpMessage = "Opens the Mare Synchronos UI"
@@ -102,14 +100,7 @@ public sealed class CommandManagerService : IDisposable
} }
else if (string.Equals(splitArgs[0], "analyze", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(splitArgs[0], "analyze", StringComparison.OrdinalIgnoreCase))
{ {
if (splitArgs.Length > 1 && string.Equals(splitArgs[1], "cancel", StringComparison.OrdinalIgnoreCase)) _mediator.Publish(new OpenDataAnalysisUiMessage());
{
_characterAnalyzer.CancelAnalyze();
}
else
{
_ = _characterAnalyzer.Analyze();
}
} }
} }
} }

View File

@@ -13,6 +13,7 @@ namespace MareSynchronos.Services.Mediator;
public record SwitchToIntroUiMessage : MessageBase; public record SwitchToIntroUiMessage : MessageBase;
public record SwitchToMainUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase;
public record OpenSettingsUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase;
public record OpenDataAnalysisUiMessage : MessageBase;
public record DalamudLoginMessage : MessageBase; public record DalamudLoginMessage : MessageBase;
public record DalamudLogoutMessage : MessageBase; public record DalamudLogoutMessage : MessageBase;
public record FrameworkUpdateMessage : SameThreadMessage; public record FrameworkUpdateMessage : SameThreadMessage;
@@ -49,6 +50,7 @@ public record NotificationMessage
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
public record HubReconnectingMessage(Exception? Exception) : MessageBase; public record HubReconnectingMessage(Exception? Exception) : MessageBase;

View File

@@ -520,6 +520,11 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.Text("No downloads in progress"); ImGui.Text("No downloads in progress");
} }
if (UiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Mare Character Data Analysis", WindowContentWidth))
{
Mediator.Publish(new OpenDataAnalysisUiMessage());
}
ImGui.SameLine(); ImGui.SameLine();
} }

View File

@@ -0,0 +1,268 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Raii;
using ImGuiNET;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace MareSynchronos.UI;
public class DataAnalysisUi : WindowMediatorSubscriberBase
{
private readonly CharacterAnalyzer _characterAnalyzer;
private bool _hasUpdate = false;
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
private string _selectedHash = string.Empty;
private ObjectKind _selectedTab;
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, MareMediator mediator, CharacterAnalyzer characterAnalyzer) : base(logger, mediator, "Mare Character Data Analysis")
{
_characterAnalyzer = characterAnalyzer;
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
{
_hasUpdate = true;
});
Mediator.Subscribe<OpenDataAnalysisUiMessage>(this, (_) => Toggle());
SizeConstraints = new()
{
MinimumSize = new()
{
X = 800,
Y = 600
},
MaximumSize = new()
{
X = 3840,
Y = 2160
}
};
}
public override void OnOpen()
{
_hasUpdate = true;
_selectedHash = string.Empty;
}
public override void Draw()
{
if (_hasUpdate)
{
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
_hasUpdate = false;
}
UiSharedService.TextWrapped("This window shows you all files and their sizes that are currently in use through your character and associated entities in Mare");
if (_cachedAnalysis!.Count == 0) return;
if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)))
{
bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
if (isAnalyzing)
{
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
ImGuiColors.DalamudYellow);
if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
{
_characterAnalyzer.CancelAnalyze();
}
}
else
{
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
ImGuiColors.DalamudYellow);
if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis"))
{
_ = _characterAnalyzer.ComputeAnalysis(false);
}
}
}
ImGui.Separator();
ImGui.TextUnformatted("Total files:");
ImGui.SameLine();
ImGui.TextUnformatted(_cachedAnalysis!.Values.Sum(c => c.Values.Count).ToString());
ImGui.SameLine();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
var groupedfiles = _cachedAnalysis.Values.SelectMany(f => f.Values).GroupBy(f => f.FileType, StringComparer.Ordinal);
text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal)
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted("Total size (uncompressed):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
ImGui.TextUnformatted("Total size (compressed):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
ImGui.Separator();
using var tabbar = ImRaii.TabBar("objectSelection");
foreach (var kvp in _cachedAnalysis)
{
using var id = ImRaii.PushId(kvp.Key.ToString());
string tabText = kvp.Key.ToString();
if (kvp.Value.Any(f => !f.Value.IsComputed)) tabText += " (!)";
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
if (tab.Success)
{
ImGui.TextUnformatted("Files for " + kvp.Key);
ImGui.SameLine();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal);
text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal)
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted($"{kvp.Key} size (uncompressed):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TextUnformatted($"{kvp.Key} size (compressed):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
ImGui.Separator();
if (_selectedTab != kvp.Key)
{
_selectedHash = string.Empty;
_selectedTab = kvp.Key;
}
using var table = ImRaii.Table("Analysis", 6, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(0, 300));
if (!table.Success) continue;
ImGui.TableSetupColumn("Type");
ImGui.TableSetupColumn("Hash");
ImGui.TableSetupColumn("Filepaths");
ImGui.TableSetupColumn("Gamepaths");
ImGui.TableSetupColumn("Original Size");
ImGui.TableSetupColumn("Compressed Size");
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
var sortSpecs = ImGui.TableGetSortSpecs();
if (sortSpecs.SpecsDirty)
{
var idx = sortSpecs.Specs.ColumnIndex;
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.FileType, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.FileType, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis[kvp.Key] = kvp.Value.OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
sortSpecs.SpecsDirty = false;
}
foreach (var item in kvp.Value)
{
using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Key, _selectedHash));
using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.Value.IsComputed);
ImGui.TableNextColumn();
if (!item.Value.IsComputed)
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudRed));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudRed));
}
if (string.Equals(_selectedHash, item.Key, StringComparison.Ordinal))
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow));
}
ImGui.TextUnformatted(item.Value.FileType);
if (ImGui.IsItemClicked()) _selectedHash = item.Key;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Key);
if (ImGui.IsItemClicked()) _selectedHash = item.Key;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Value.FilePaths.Count.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Key;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Value.GamePaths.Count.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Key;
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(item.Value.OriginalSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Key;
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(item.Value.CompressedSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Key;
}
}
}
ImGui.Separator();
ImGui.Text("Selected file:");
ImGui.SameLine();
UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow);
if (_cachedAnalysis[_selectedTab].ContainsKey(_selectedHash))
{
var filePaths = _cachedAnalysis[_selectedTab][_selectedHash].FilePaths;
ImGui.TextUnformatted("Local file path:");
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
UiSharedService.FontText(FontAwesomeIcon.InfoCircle.ToIconString(), UiBuilder.IconFont);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = _cachedAnalysis[_selectedTab][_selectedHash].GamePaths;
ImGui.TextUnformatted("Used by game path:");
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
UiSharedService.FontText(FontAwesomeIcon.InfoCircle.ToIconString(), UiBuilder.IconFont);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
}
}

View File

@@ -32,7 +32,11 @@ public sealed class DtrEntry : IDisposable, IHostedService
public void Dispose() public void Dispose()
{ {
_entry.Value.Dispose(); if (_entry.IsValueCreated)
{
_logger.LogDebug("Disposing DtrEntry");
Clear();
}
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -51,15 +55,6 @@ public sealed class DtrEntry : IDisposable, IHostedService
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
finally finally
{ {
_logger.LogDebug("Disposing DtrEntry");
if (_entry.IsValueCreated)
{
Clear();
_entry.Value.Remove();
_entry.Value.Dispose();
}
_cancellationTokenSource.Dispose(); _cancellationTokenSource.Dispose();
} }
} }

View File

@@ -29,7 +29,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new(); private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly FileUploadManager _fileTransferManager; private readonly FileUploadManager _fileTransferManager;
private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly MareCharaFileManager _mareCharaFileManager; private readonly MareCharaFileManager _mareCharaFileManager;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
@@ -51,8 +50,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ServerConfigurationManager serverConfigurationManager, ServerConfigurationManager serverConfigurationManager,
MareMediator mediator, PerformanceCollectorService performanceCollector, MareMediator mediator, PerformanceCollectorService performanceCollector,
FileUploadManager fileTransferManager, FileUploadManager fileTransferManager,
FileTransferOrchestrator fileTransferOrchestrator, FileTransferOrchestrator fileTransferOrchestrator) : base(logger, mediator, "Mare Synchronos Settings")
CharacterAnalyzer characterAnalyzer) : base(logger, mediator, "Mare Synchronos Settings")
{ {
_configService = configService; _configService = configService;
_mareCharaFileManager = mareCharaFileManager; _mareCharaFileManager = mareCharaFileManager;
@@ -61,7 +59,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_performanceCollector = performanceCollector; _performanceCollector = performanceCollector;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_fileTransferOrchestrator = fileTransferOrchestrator; _fileTransferOrchestrator = fileTransferOrchestrator;
_characterAnalyzer = characterAnalyzer;
_uiShared = uiShared; _uiShared = uiShared;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
@@ -334,23 +331,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server."); UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server.");
var isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
if (isAnalyzing) ImGui.BeginDisabled();
if (UiSharedService.IconTextButton(FontAwesomeIcon.QuestionCircle, "[DEBUG] Analyze current character composition to /xllog"))
{
_ = _characterAnalyzer.Analyze();
}
UiSharedService.AttachToolTip("This will compute your current \"Mare load\" and print it to the /xllog");
if (isAnalyzing) ImGui.EndDisabled();
ImGui.SameLine();
if (!isAnalyzing) ImGui.BeginDisabled();
if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
{
_characterAnalyzer.CancelAnalyze();
}
UiSharedService.AttachToolTip("Cancels the current analysis of your character composition");
if (!isAnalyzing) ImGui.EndDisabled();
_uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) => _uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) =>
{ {
_configService.Current.LogLevel = l; _configService.Current.LogLevel = l;

View File

@@ -313,7 +313,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
} }
public static bool IconTextButton(FontAwesomeIcon icon, string text) public static bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null)
{ {
var buttonClicked = false; var buttonClicked = false;
@@ -322,9 +322,18 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
var padding = ImGui.GetStyle().FramePadding; var padding = ImGui.GetStyle().FramePadding;
var spacing = ImGui.GetStyle().ItemSpacing; var spacing = ImGui.GetStyle().ItemSpacing;
var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X; Vector2 buttonSize;
var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2; var buttonSizeY = (iconSize.Y > textSize.Y ? iconSize.Y : textSize.Y) + padding.Y * 2;
var buttonSize = new Vector2(buttonSizeX, buttonSizeY);
if (width == null)
{
var buttonSizeX = iconSize.X + textSize.X + padding.X * 2 + spacing.X;
buttonSize = new Vector2(buttonSizeX, buttonSizeY);
}
else
{
buttonSize = new Vector2(width.Value, buttonSizeY);
}
if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize)) if (ImGui.Button("###" + icon.ToIconString() + text, buttonSize))
{ {