Skip to content

Commit 27876f8

Browse files
committed
Avoid list copy on every update
By changing the set of subscriptions to an immutable collection and using optimistic locking to update it, we avoid having to copy the list of subscribers within a lock on every update, reducing allocations and the scope of synchronised blocks.
1 parent f2788ea commit 27876f8

File tree

1 file changed

+9
-13
lines changed

1 file changed

+9
-13
lines changed

src/Aspire.Hosting/Dashboard/ViewModelProcessor.cs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Immutable;
45
using System.Threading.Channels;
56
using Aspire.Dashboard.Model;
67

@@ -13,7 +14,7 @@ internal sealed class ViewModelProcessor<TViewModel>
1314
private readonly Channel<ResourceChanged<TViewModel>> _incomingChannel;
1415
private readonly CancellationToken _cancellationToken;
1516
private readonly Dictionary<string, TViewModel> _snapshot = [];
16-
private readonly List<Channel<ResourceChanged<TViewModel>>> _subscribedChannels = [];
17+
private ImmutableHashSet<Channel<ResourceChanged<TViewModel>>> _outgoingChannels = [];
1718

1819
public ViewModelProcessor(Channel<ResourceChanged<TViewModel>> incomingChannel, CancellationToken cancellationToken)
1920
{
@@ -28,18 +29,15 @@ public ViewModelMonitor<TViewModel> GetResourceMonitor()
2829
{
2930
var snapshot = _snapshot.Values.ToList();
3031
var channel = Channel.CreateUnbounded<ResourceChanged<TViewModel>>();
31-
_subscribedChannels.Add(channel);
32+
ImmutableInterlocked.Update(ref _outgoingChannels, static (set, channel) => set.Add(channel), channel);
3233
var enumerable = new ChangeEnumerable(channel, RemoveChannel);
3334

3435
return new ViewModelMonitor<TViewModel>(snapshot, enumerable);
3536
}
36-
}
3737

38-
private void RemoveChannel(Channel<ResourceChanged<TViewModel>> channel)
39-
{
40-
lock (_syncLock)
38+
void RemoveChannel(Channel<ResourceChanged<TViewModel>> channel)
4139
{
42-
_subscribedChannels.Remove(channel);
40+
ImmutableInterlocked.Update(ref _outgoingChannels, static (set, channel) => set.Remove(channel), channel);
4341
}
4442
}
4543

@@ -96,11 +94,11 @@ private async Task ProcessChanges()
9694
{
9795
await foreach (var change in _incomingChannel.Reader.ReadAllAsync(_cancellationToken))
9896
{
99-
List<Channel<ResourceChanged<TViewModel>>> outgoingChannels;
97+
var (changeType, resource) = change;
98+
10099
lock (_syncLock)
101100
{
102-
var resource = change.Resource;
103-
switch (change.ObjectChangeType)
101+
switch (changeType)
104102
{
105103
case ObjectChangeType.Added:
106104
_snapshot.Add(resource.Name, resource);
@@ -114,11 +112,9 @@ private async Task ProcessChanges()
114112
_snapshot.Remove(resource.Name);
115113
break;
116114
}
117-
118-
outgoingChannels = _subscribedChannels.ToList();
119115
}
120116

121-
foreach (var channel in outgoingChannels)
117+
foreach (var channel in _outgoingChannels)
122118
{
123119
await channel.Writer.WriteAsync(change, _cancellationToken).ConfigureAwait(false);
124120
}

0 commit comments

Comments
 (0)