From 480558d58ea91b33d7a01051b93dd3bd0a4c05d1 Mon Sep 17 00:00:00 2001 From: Wei-Lun Li Date: Fri, 13 Nov 2026 06:39:29 +0000 Subject: [PATCH] perf: look up ProfileEx items by IndexId instead of scanning the list (#10588) ProfileExManager kept its entries in a ConcurrentBag and found them with FirstOrDefault, which copies and scans the whole bag on every lookup. Recording test results, SaveTo, MoveServer and subscription imports were quadratic in the number of servers. Store the entries in a ConcurrentDictionary keyed by IndexId and track the highest sort in SetSort so GetMaxSort no longer scans. --- .../ProfileExManagerConcurrencyTests.cs | 12 +++++ v2rayN/ServiceLib/Manager/ProfileExManager.cs | 48 +++++++++---------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerConcurrencyTests.cs b/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerConcurrencyTests.cs index 845da5c3..fd263079 100644 --- a/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerConcurrencyTests.cs +++ b/v2rayN/ServiceLib.Tests/Manager/ProfileExManagerConcurrencyTests.cs @@ -93,6 +93,18 @@ public class ProfileExManagerConcurrencyTests } } + [Test] + public async Task SetSort_FromManyThreads_ShouldTrackMaxSort() + { + var manager = CreateManager(); + await manager.GetMaxSort().Should().BeEqualTo(0); + + Parallel.For(0, 1000, i => manager.SetSort($"sort{i % 100}", i)); + manager.SetTestDelay("unsorted", 100); + + await manager.GetMaxSort().Should().BeEqualTo(999); + } + private static async Task RunConcurrentUpdates(int workers, int indexes, Action update) { using var barrier = new Barrier(workers); diff --git a/v2rayN/ServiceLib/Manager/ProfileExManager.cs b/v2rayN/ServiceLib/Manager/ProfileExManager.cs index cc0f9a74..1c361716 100644 --- a/v2rayN/ServiceLib/Manager/ProfileExManager.cs +++ b/v2rayN/ServiceLib/Manager/ProfileExManager.cs @@ -3,9 +3,10 @@ namespace ServiceLib.Manager; public class ProfileExManager { private static readonly Lazy _instance = new(() => new()); - private ConcurrentBag _lstProfileEx = []; + private ConcurrentDictionary _dicProfileEx = new(); private readonly ConcurrentQueue _queIndexIds = new(); private readonly Lock _profileExLock = new(); + private int _maxSort; public static ProfileExManager Instance => _instance.Value; private static readonly string _tag = "ProfileExHandler"; @@ -19,19 +20,24 @@ public class ProfileExManager await InitData(); } - public async Task> GetProfileExs() + public async Task> GetProfileExs() { - return await Task.FromResult(_lstProfileEx); + return await Task.FromResult(_dicProfileEx.Values); } private async Task InitData() { await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )"); - var lstProfileEx = new ConcurrentBag(await SQLiteHelper.Instance.TableAsync().ToListAsync()); + var dicProfileEx = new ConcurrentDictionary(); + foreach (var item in await SQLiteHelper.Instance.TableAsync().ToListAsync()) + { + dicProfileEx[item.IndexId] = item; + } lock (_profileExLock) { - _lstProfileEx = lstProfileEx; + _dicProfileEx = dicProfileEx; + _maxSort = dicProfileEx.Values.Select(t => t.Sort).DefaultIfEmpty(0).Max(); } } @@ -58,8 +64,7 @@ public class ProfileExManager foreach (var id in lstIds) { - var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id); - if (itemNew is null) + if (!_dicProfileEx.TryGetValue(id, out var itemNew)) { continue; } @@ -92,23 +97,22 @@ public class ProfileExManager Sort = 0, Message = string.Empty }; - _lstProfileEx.Add(profileEx); + _dicProfileEx[indexId] = profileEx; IndexIdEnqueue(indexId); return profileEx; } private ProfileExItem GetProfileExItem(string? indexId) { - var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); - if (profileEx is not null) + indexId ??= string.Empty; + if (_dicProfileEx.TryGetValue(indexId, out var profileEx)) { return profileEx; } lock (_profileExLock) { - profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); - return profileEx ?? AddProfileEx(indexId); + return _dicProfileEx.TryGetValue(indexId, out profileEx) ? profileEx : AddProfileEx(indexId); } } @@ -117,7 +121,8 @@ public class ProfileExManager await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem "); lock (_profileExLock) { - _lstProfileEx = []; + _dicProfileEx = new(); + _maxSort = 0; } } @@ -171,24 +176,19 @@ public class ProfileExManager profileEx.Sort = sort; IndexIdEnqueue(indexId); + lock (_profileExLock) + { + _maxSort = Math.Max(_maxSort, sort); + } } public int GetSort(string indexId) { - var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); - if (profileEx == null) - { - return 0; - } - return profileEx.Sort; + return _dicProfileEx.TryGetValue(indexId, out var profileEx) ? profileEx.Sort : 0; } public int GetMaxSort() { - if (_lstProfileEx.Count <= 0) - { - return 0; - } - return _lstProfileEx.Max(t => t?.Sort ?? 0); + return _maxSort; } }