Fix slowness issues with FilesTab when torrents with large file lists are being rendered.

This commit is contained in:
ahjephson
2025-10-19 11:06:45 +01:00
parent b24ae440d4
commit bb524450f0
3 changed files with 118 additions and 60 deletions

View File

@@ -8,6 +8,7 @@ using Lantean.QBTMud.Models;
using Lantean.QBTMud.Services; using Lantean.QBTMud.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Net; using System.Net;
@@ -368,16 +369,16 @@ namespace Lantean.QBTMud.Components
return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder); return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
} }
private IEnumerable<ContentItem> GetChildren(ContentItem folder, int level) private IEnumerable<ContentItem> GetChildren(ContentItem folder)
{ {
level++; var childLevel = folder.Level + 1;
var descendantsKey = folder.GetDescendantsKey(level); var prefix = string.Concat(folder.Name, Extensions.DirectorySeparator);
foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey) && f.Level == level).OrderByDirection(_sortDirection, GetSortSelector())) foreach (var item in FileList!.Values.Where(f => f.Level == childLevel && f.Name.StartsWith(prefix, StringComparison.Ordinal)).OrderByDirection(_sortDirection, GetSortSelector()))
{ {
if (item.IsFolder) if (item.IsFolder)
{ {
var descendants = GetChildren(item, level); var descendants = GetChildren(item);
// if the filter returns some results then show folder item // if the filter returns some results then show folder item
if (descendants.Any()) if (descendants.Any())
{ {
@@ -451,8 +452,7 @@ namespace Lantean.QBTMud.Components
if (item.IsFolder && ExpandedNodes.Contains(item.Name)) if (item.IsFolder && ExpandedNodes.Contains(item.Name))
{ {
var level = 0; var descendants = GetChildren(item);
var descendants = GetChildren(item, level);
foreach (var descendant in descendants) foreach (var descendant in descendants)
{ {
list.Add(descendant); list.Add(descendant);
@@ -552,4 +552,4 @@ namespace Lantean.QBTMud.Components
ColumnDefinitionHelper.CreateColumnDefinition<ContentItem>("Availability", c => c.Availability, c => c.Availability.ToString("0.00")), ColumnDefinitionHelper.CreateColumnDefinition<ContentItem>("Availability", c => c.Availability, c => c.Availability.ToString("0.00")),
]; ];
} }
} }

View File

@@ -625,82 +625,140 @@ namespace Lantean.QBTMud.Services
public Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files) public Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files)
{ {
var contents = new Dictionary<string, ContentItem>(); return BuildContentsTree(files);
}
private static Dictionary<string, ContentItem> BuildContentsTree(IReadOnlyList<QBitTorrentClient.Models.FileData> files)
{
var result = new Dictionary<string, ContentItem>();
if (files.Count == 0) if (files.Count == 0)
{ {
return contents; return result;
} }
var folderIndex = files.Min(f => f.Index) - 1; var folderIndex = files.Min(f => f.Index) - 1;
var nodes = new Dictionary<string, ContentTreeNode>(files.Count * 2);
var root = new ContentTreeNode(null, null);
foreach (var file in files) foreach (var file in files)
{ {
if (!file.Name.Contains(Extensions.DirectorySeparator)) var parent = root;
string? parentPath = parent.Item?.Name;
var segments = file.Name.Split(Extensions.DirectorySeparator);
var directoriesLength = segments.Length - 1;
for (var i = 0; i < directoriesLength; i++)
{ {
contents.Add(file.Name, new ContentItem(file.Name, file.Name, file.Index, (Priority)(int)file.Priority, file.Progress, file.Size, file.Availability)); var folderName = segments[i];
} if (folderName == ".unwanted")
else
{
var nameAndPath = file.Name.Split(Extensions.DirectorySeparator);
var paths = nameAndPath[..^1];
for (var i = 0; i < paths.Length; i++)
{ {
var directoryName = paths[i]; continue;
var directoryPath = string.Join(Extensions.DirectorySeparator, paths[0..(i + 1)]);
if (!contents.ContainsKey(directoryPath))
{
contents.Add(directoryPath, new ContentItem(directoryPath, directoryName, folderIndex--, Priority.Normal, 0, 0, 0, true, i));
}
} }
var displayName = nameAndPath[^1]; var folderPath = string.IsNullOrEmpty(parentPath)
? folderName
: string.Concat(parentPath, Extensions.DirectorySeparator, folderName);
contents.Add(file.Name, new ContentItem(file.Name, displayName, file.Index, (Priority)(int)file.Priority, file.Progress, file.Size, file.Availability, false, paths.Length)); if (!nodes.TryGetValue(folderPath, out var folderNode))
{
var level = (parent.Item?.Level ?? -1) + 1;
var folderItem = new ContentItem(folderPath, folderName, folderIndex--, Priority.Normal, 0, 0, 0, true, level);
folderNode = new ContentTreeNode(folderItem, parent);
nodes[folderPath] = folderNode;
parent.Children[folderPath] = folderNode;
}
parent = folderNode;
parentPath = parent.Item!.Name;
} }
var displayName = segments[^1];
var fileLevel = (parent.Item?.Level ?? -1) + 1;
var fileItem = new ContentItem(file.Name, displayName, file.Index, (Priority)(int)file.Priority, file.Progress, file.Size, file.Availability, false, fileLevel);
var fileNode = new ContentTreeNode(fileItem, parent);
nodes[file.Name] = fileNode;
parent.Children[fileItem.Name] = fileNode;
} }
var directories = contents.Where(c => c.Value.IsFolder).OrderByDescending(c => c.Value.Level); var folders = nodes.Values
.Where(n => n.Item is not null && n.Item.IsFolder)
.OrderByDescending(n => n.Item!.Level)
.ToList();
foreach (var directory in directories) foreach (var folder in folders)
{ {
var key = directory.Key; var folderItem = folder.Item!;
var level = directory.Value.Level; if (folder.Children.Count == 0)
var filesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && !c.Value.IsFolder).ToList();
var directoriesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && c.Value.IsFolder && c.Value.Level == level + 1).ToList();
var allContents = filesContents.Concat(directoriesContents);
var priorities = allContents.Select(d => d.Value.Priority).Distinct();
var downloadingContents = allContents.Where(c => c.Value.Priority != Priority.DoNotDownload && !c.Value.IsFolder).ToList();
long size = 0;
float availability = 0;
long downloaded = 0;
float progress = 0;
if (downloadingContents.Count != 0)
{ {
size = downloadingContents.Sum(c => c.Value.Size); folderItem.Size = 0;
availability = downloadingContents.Average(c => c.Value.Availability); folderItem.Progress = 0;
downloaded = downloadingContents.Sum(c => c.Value.Downloaded); folderItem.Availability = 0;
progress = (float)downloaded / size; folderItem.Priority = Priority.Normal;
continue;
} }
if (!contents.TryGetValue(key, out var dir)) long sizeSum = 0;
double progressSum = 0;
double availabilitySum = 0;
var firstChild = true;
var aggregatedPriority = Priority.Normal;
foreach (var child in folder.Children.Values)
{
var childItem = child.Item!;
sizeSum += childItem.Size;
if (firstChild)
{
aggregatedPriority = childItem.Priority;
firstChild = false;
}
else if (aggregatedPriority != childItem.Priority)
{
aggregatedPriority = Priority.Mixed;
}
if (childItem.Priority != Priority.DoNotDownload)
{
progressSum += childItem.Progress * childItem.Size;
availabilitySum += childItem.Availability * childItem.Size;
}
}
folderItem.Size = sizeSum;
folderItem.Progress = sizeSum > 0 ? (float)(progressSum / sizeSum) : 0;
folderItem.Availability = sizeSum > 0 ? (float)(availabilitySum / sizeSum) : 0;
folderItem.Priority = firstChild ? Priority.Normal : aggregatedPriority;
}
foreach (var node in nodes.Values)
{
if (node.Item is null)
{ {
continue; continue;
} }
dir.Availability = availability;
dir.Size = size; result[node.Item.Name] = node.Item;
dir.Progress = progress;
if (priorities.Count() == 1)
{
dir.Priority = priorities.First();
}
else
{
dir.Priority = Priority.Mixed;
}
} }
return contents; return result;
}
private sealed class ContentTreeNode
{
public ContentTreeNode(ContentItem? item, ContentTreeNode? parent)
{
Item = item;
Parent = parent;
Children = new Dictionary<string, ContentTreeNode>();
}
public ContentItem? Item { get; }
public ContentTreeNode? Parent { get; }
public Dictionary<string, ContentTreeNode> Children { get; }
} }
public QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed) public QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed)
@@ -1175,4 +1233,4 @@ namespace Lantean.QBTMud.Services
return new RssList(feeds, articles); return new RssList(feeds, articles);
} }
} }
} }

View File

@@ -22,4 +22,4 @@ namespace Lantean.QBTMud.Services
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems); RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
} }
} }