Skip to content

Commit 67afd98

Browse files
kjy5Copilot
andauthored
Fix manipulator memory leak (#845)
* Switch demo to use set * WIP remove visualiztion update on every state change and only update manipulators that change * WIP iterative loop? * Use magnitude detection * Enable faster vis * Update Assets/Scripts/Services/EphysLinkService.cs Co-authored-by: Copilot <[email protected]> * Update Assets/Scripts/Services/EphysLinkService.cs Co-authored-by: Copilot <[email protected]> * Update Assets/Scripts/Services/EphysLinkService.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent ce3ac77 commit 67afd98

File tree

2 files changed

+121
-32
lines changed

2 files changed

+121
-32
lines changed

Assets/Scripts/Services/EphysLinkService.cs

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Utils;
2020
using Utils.Types;
2121
using Action = System.Action;
22+
using System.Threading;
2223

2324
namespace Services
2425
{
@@ -31,6 +32,9 @@ public class EphysLinkService
3132
// FIXME: This should go into some common constants file (along with copy in probe inspector view model).
3233
private readonly Vector2 _pitchRange = new(0, 90);
3334

35+
// Visualization loop interval (ms)
36+
private const int VISUALIZATION_UPDATE_INTERVAL_MS = 10; // about 60 Hz
37+
3438
#endregion
3539

3640
#region Services
@@ -48,12 +52,15 @@ public class EphysLinkService
4852

4953
public string SocketId => _socket.Id;
5054

55+
// Cancellation for the visualization update loop
56+
private CancellationTokenSource _visualizationLoopCts;
57+
5158
#endregion
5259

5360
#region Demo Loop
5461

55-
private readonly Dictionary<string, bool> _runningDemoLoops = new();
56-
private const float DEMO_SPEED = 100f; // µm/s
62+
private readonly HashSet<string> _runningDemoLoops = new();
63+
private const float DEMO_SPEED = 0.1f; // mm/s
5764
#endregion
5865

5966
public EphysLinkService(StoreService storeService)
@@ -72,36 +79,31 @@ public EphysLinkService(StoreService storeService)
7279

7380
private async void OnSceneStateChanged(SceneState sceneState)
7481
{
75-
// Apply small delay to prevent overrunning updates (delay for roughly 60 FPS).
76-
await Task.Delay(10);
77-
7882
// Handle demo loop state changes.
7983
foreach (var manipulatorState in sceneState.Manipulators)
8084
{
81-
_runningDemoLoops.TryGetValue(manipulatorState.Id, out var isRunning);
85+
var isRunning = _runningDemoLoops.Contains(manipulatorState.Id);
8286

8387
switch (manipulatorState.IsDemoRunning)
8488
{
8589
// Start demo loop if requested and not already running.
8690
case true when !isRunning:
87-
_runningDemoLoops[manipulatorState.Id] = true;
91+
_runningDemoLoops.Add(manipulatorState.Id);
8892
_ = RunDemoLoop(manipulatorState.Id);
8993
break;
9094
// Stop demo loop if requested and currently running.
9195
case false when isRunning:
9296
await Stop(manipulatorState.Id);
97+
_runningDemoLoops.Remove(manipulatorState.Id);
9398
break;
9499
}
95100
}
96-
97-
// WARNING: this will create an infinite loop of state updates on purpose.
98-
// Update the position of visualization probes.
99-
await UpdateVisualizationProbePosition(sceneState);
100101
}
101102

102103
private void OnShuttingDown()
103104
{
104105
_sceneStateSubscription.Dispose();
106+
StopVisualizationLoop();
105107
App.shuttingDown -= OnShuttingDown;
106108
}
107109

@@ -125,6 +127,9 @@ public void ConnectToServer(
125127
if (_socketManager != null && _socketManager.Socket.IsOpen)
126128
_socketManager.Close();
127129

130+
// Ensure any previous loop is stopped.
131+
StopVisualizationLoop();
132+
128133
// Create new connection.
129134
var options = new SocketOptions { Timeout = new TimeSpan(0, 0, 2) };
130135

@@ -135,10 +140,10 @@ public void ConnectToServer(
135140
_socketManager = new SocketManager(new Uri($"http://{ip}:{port}"), options);
136141
_socket = _socketManager.Socket;
137142

138-
// On successful connection.
139-
_socket.Once(
140-
"connect",
141-
async () =>
143+
// Local async handler to run after successful connection.
144+
async Task OnConnectedAsync()
145+
{
146+
try
142147
{
143148
// Check version compatibility.
144149
if (await IsVersionCompatible())
@@ -153,12 +158,38 @@ public void ConnectToServer(
153158
(EphysLinkConnectionState.Connected, _socket.Id)
154159
);
155160
onConnected?.Invoke();
161+
162+
// Start visualization update loop until disconnect.
163+
StartVisualizationLoop();
156164
}
157165
else
158166
{
159167
HandleError(GetOutdatedVersionErrorMessage());
160168
}
161169
}
170+
catch (Exception ex)
171+
{
172+
HandleError($"{GetErrorConnectingToServerMessage()} Caused exception: {ex.Message}");
173+
}
174+
}
175+
176+
// When the server disconnects, stop the visualization loop and update state.
177+
_socket.On(
178+
"disconnect",
179+
() =>
180+
{
181+
StopVisualizationLoop();
182+
_storeService.Store.Dispatch(
183+
SettingsActions.SET_EPHYS_LINK_CONNECTION_STATE,
184+
(EphysLinkConnectionState.Disconnected, "")
185+
);
186+
}
187+
);
188+
189+
// On successful connection, delegate to async task handler.
190+
_socket.Once(
191+
"connect",
192+
() => { _ = OnConnectedAsync(); }
162193
);
163194

164195
// On error.
@@ -202,6 +233,9 @@ void HandleError(string message)
202233
/// <param name="onDisconnected">Post disconnection behavior.</param>
203234
public void Disconnect(Action onDisconnected = null)
204235
{
236+
// Stop the visualization loop if running.
237+
StopVisualizationLoop();
238+
205239
// Close socket connection.
206240
_socketManager?.Close();
207241
_socketManager = null;
@@ -527,6 +561,61 @@ private static string ToJson<T>(T data)
527561
return JsonUtility.ToJson(data);
528562
}
529563

564+
// Starts the continuous visualization update loop until the socket disconnects or service disconnects.
565+
private void StartVisualizationLoop()
566+
{
567+
StopVisualizationLoop(); // ensure only one loop runs
568+
_visualizationLoopCts = new CancellationTokenSource();
569+
var token = _visualizationLoopCts.Token;
570+
_ = VisualizationUpdateLoop(token);
571+
}
572+
573+
// Cancels and disposes the visualization update loop.
574+
private void StopVisualizationLoop()
575+
{
576+
if (_visualizationLoopCts != null)
577+
{
578+
try { _visualizationLoopCts.Cancel(); }
579+
catch (ObjectDisposedException) { /* ignore disposed */ }
580+
catch (Exception ex)
581+
{
582+
Debug.Log($"Ignored exception during visualization loop cancellation: {ex}");
583+
}
584+
_visualizationLoopCts.Dispose();
585+
_visualizationLoopCts = null;
586+
}
587+
}
588+
589+
// The loop body calling UpdateVisualizationProbePosition at a fixed interval.
590+
private async Task VisualizationUpdateLoop(CancellationToken token)
591+
{
592+
while (!token.IsCancellationRequested)
593+
{
594+
try
595+
{
596+
var sceneState = _storeService.Store.GetState<SceneState>(SliceNames.SCENE_SLICE);
597+
await UpdateVisualizationProbePosition(sceneState);
598+
}
599+
catch (OperationCanceledException)
600+
{
601+
break;
602+
}
603+
catch (Exception ex)
604+
{
605+
Debug.LogWarning($"Visualization update loop error: {ex.Message}");
606+
}
607+
608+
try
609+
{
610+
await Task.Delay(VISUALIZATION_UPDATE_INTERVAL_MS, token);
611+
}
612+
catch (OperationCanceledException)
613+
{
614+
break;
615+
}
616+
}
617+
}
618+
530619
#endregion
531620

532621
#region Visualization control
@@ -611,6 +700,13 @@ var manipulatorState in sceneState.Manipulators.Where(state =>
611700
var transformedAPMLDV = BrainAtlasManager.World2T_Vector(
612701
referenceCoordinateAdjustedWorldPosition
613702
);
703+
704+
// Cancel update if the manipulator's position did not change by a lot.
705+
var probeState = sceneState.Probes.FirstOrDefault(state => state.Name == manipulatorState.VisualizationProbeName);
706+
if (probeState == null || Vector3.SqrMagnitude(transformedAPMLDV - probeState.APMLDV) < 0.0001f)
707+
{
708+
continue;
709+
}
614710

615711
// Get the current forward vector of the probe.
616712
var forwardT = BrainAtlasManager.ActiveAtlasTransform.U2T_Vector(
@@ -782,14 +878,14 @@ private async Task RunDemoLoop(string manipulatorId)
782878
// Exit if manipulator not found.
783879
if (manipulatorState == null)
784880
{
785-
_runningDemoLoops[manipulatorId] = false;
881+
_runningDemoLoops.Remove(manipulatorId);
786882
return;
787883
}
788884

789885
// Exit if demo is no longer running.
790886
if (!manipulatorState.IsDemoRunning)
791887
{
792-
_runningDemoLoops[manipulatorId] = false;
888+
_runningDemoLoops.Remove(manipulatorId);
793889
return;
794890
}
795891

@@ -804,7 +900,7 @@ private async Task RunDemoLoop(string manipulatorId)
804900
// Exit on error (including stop request).
805901
if (HasError(homeResponse.Error))
806902
{
807-
_runningDemoLoops[manipulatorId] = false;
903+
_runningDemoLoops.Remove(manipulatorId);
808904
return;
809905
}
810906

@@ -815,7 +911,7 @@ private async Task RunDemoLoop(string manipulatorId)
815911
);
816912
if (manipulatorState is not { IsDemoRunning: true })
817913
{
818-
_runningDemoLoops[manipulatorId] = false;
914+
_runningDemoLoops.Remove(manipulatorId);
819915
return;
820916
}
821917

@@ -836,7 +932,7 @@ private async Task RunDemoLoop(string manipulatorId)
836932
// Exit on error (including stop request).
837933
if (HasError(intermediateResponse.Error))
838934
{
839-
_runningDemoLoops[manipulatorId] = false;
935+
_runningDemoLoops.Remove(manipulatorId);
840936
return;
841937
}
842938

@@ -847,7 +943,7 @@ private async Task RunDemoLoop(string manipulatorId)
847943
);
848944
if (manipulatorState is not { IsDemoRunning: true })
849945
{
850-
_runningDemoLoops[manipulatorId] = false;
946+
_runningDemoLoops.Remove(manipulatorId);
851947
return;
852948
}
853949

@@ -862,7 +958,7 @@ private async Task RunDemoLoop(string manipulatorId)
862958
// Exit on error (including stop request).
863959
if (HasError(targetResponse.Error))
864960
{
865-
_runningDemoLoops[manipulatorId] = false;
961+
_runningDemoLoops.Remove(manipulatorId);
866962
return;
867963
}
868964

@@ -873,7 +969,7 @@ private async Task RunDemoLoop(string manipulatorId)
873969
);
874970
if (manipulatorState is not { IsDemoRunning: true })
875971
{
876-
_runningDemoLoops[manipulatorId] = false;
972+
_runningDemoLoops.Remove(manipulatorId);
877973
return;
878974
}
879975

@@ -894,7 +990,7 @@ private async Task RunDemoLoop(string manipulatorId)
894990
// Exit on error (including stop request).
895991
if (HasError(returnToIntermediateResponse.Error))
896992
{
897-
_runningDemoLoops[manipulatorId] = false;
993+
_runningDemoLoops.Remove(manipulatorId);
898994
return;
899995
}
900996

@@ -909,7 +1005,7 @@ private async Task RunDemoLoop(string manipulatorId)
9091005
continue;
9101006
}
9111007

912-
_runningDemoLoops[manipulatorId] = false;
1008+
_runningDemoLoops.Remove(manipulatorId);
9131009
return;
9141010
}
9151011
}

Assets/settings_conversion.txt.meta

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)