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.
This commit is contained in:
Wei-Lun Li
2026-11-13 06:39:29 +00:00
committed by GitHub
parent 28f8ca7ba1
commit 480558d58e
2 changed files with 36 additions and 24 deletions
@@ -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<int, int> update) private static async Task RunConcurrentUpdates(int workers, int indexes, Action<int, int> update)
{ {
using var barrier = new Barrier(workers); using var barrier = new Barrier(workers);
+24 -24
View File
@@ -3,9 +3,10 @@ namespace ServiceLib.Manager;
public class ProfileExManager public class ProfileExManager
{ {
private static readonly Lazy<ProfileExManager> _instance = new(() => new()); private static readonly Lazy<ProfileExManager> _instance = new(() => new());
private ConcurrentBag<ProfileExItem> _lstProfileEx = []; private ConcurrentDictionary<string, ProfileExItem> _dicProfileEx = new();
private readonly ConcurrentQueue<string> _queIndexIds = new(); private readonly ConcurrentQueue<string> _queIndexIds = new();
private readonly Lock _profileExLock = new(); private readonly Lock _profileExLock = new();
private int _maxSort;
public static ProfileExManager Instance => _instance.Value; public static ProfileExManager Instance => _instance.Value;
private static readonly string _tag = "ProfileExHandler"; private static readonly string _tag = "ProfileExHandler";
@@ -19,19 +20,24 @@ public class ProfileExManager
await InitData(); await InitData();
} }
public async Task<ConcurrentBag<ProfileExItem>> GetProfileExs() public async Task<ICollection<ProfileExItem>> GetProfileExs()
{ {
return await Task.FromResult(_lstProfileEx); return await Task.FromResult(_dicProfileEx.Values);
} }
private async Task InitData() private async Task InitData()
{ {
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )"); await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )");
var lstProfileEx = new ConcurrentBag<ProfileExItem>(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync()); var dicProfileEx = new ConcurrentDictionary<string, ProfileExItem>();
foreach (var item in await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync())
{
dicProfileEx[item.IndexId] = item;
}
lock (_profileExLock) 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) foreach (var id in lstIds)
{ {
var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id); if (!_dicProfileEx.TryGetValue(id, out var itemNew))
if (itemNew is null)
{ {
continue; continue;
} }
@@ -92,23 +97,22 @@ public class ProfileExManager
Sort = 0, Sort = 0,
Message = string.Empty Message = string.Empty
}; };
_lstProfileEx.Add(profileEx); _dicProfileEx[indexId] = profileEx;
IndexIdEnqueue(indexId); IndexIdEnqueue(indexId);
return profileEx; return profileEx;
} }
private ProfileExItem GetProfileExItem(string? indexId) private ProfileExItem GetProfileExItem(string? indexId)
{ {
var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); indexId ??= string.Empty;
if (profileEx is not null) if (_dicProfileEx.TryGetValue(indexId, out var profileEx))
{ {
return profileEx; return profileEx;
} }
lock (_profileExLock) lock (_profileExLock)
{ {
profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); return _dicProfileEx.TryGetValue(indexId, out profileEx) ? profileEx : AddProfileEx(indexId);
return profileEx ?? AddProfileEx(indexId);
} }
} }
@@ -117,7 +121,8 @@ public class ProfileExManager
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem "); await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
lock (_profileExLock) lock (_profileExLock)
{ {
_lstProfileEx = []; _dicProfileEx = new();
_maxSort = 0;
} }
} }
@@ -171,24 +176,19 @@ public class ProfileExManager
profileEx.Sort = sort; profileEx.Sort = sort;
IndexIdEnqueue(indexId); IndexIdEnqueue(indexId);
lock (_profileExLock)
{
_maxSort = Math.Max(_maxSort, sort);
}
} }
public int GetSort(string indexId) public int GetSort(string indexId)
{ {
var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId); return _dicProfileEx.TryGetValue(indexId, out var profileEx) ? profileEx.Sort : 0;
if (profileEx == null)
{
return 0;
}
return profileEx.Sort;
} }
public int GetMaxSort() public int GetMaxSort()
{ {
if (_lstProfileEx.Count <= 0) return _maxSort;
{
return 0;
}
return _lstProfileEx.Max(t => t?.Sort ?? 0);
} }
} }