diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs
index ace53d4..918f463 100644
--- a/allstarr/Controllers/AdminController.cs
+++ b/allstarr/Controllers/AdminController.cs
@@ -1911,23 +1911,36 @@ public class AdminController : ControllerBase
{
try
{
+ // Get memory stats BEFORE GC
+ var memoryBeforeGC = GC.GetTotalMemory(false);
+ var gen0Before = GC.CollectionCount(0);
+ var gen1Before = GC.CollectionCount(1);
+ var gen2Before = GC.CollectionCount(2);
+
// Force garbage collection to get accurate numbers
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
- var memoryUsage = GC.GetTotalMemory(false);
- var gen0 = GC.CollectionCount(0);
- var gen1 = GC.CollectionCount(1);
- var gen2 = GC.CollectionCount(2);
+ var memoryAfterGC = GC.GetTotalMemory(false);
+ var gen0After = GC.CollectionCount(0);
+ var gen1After = GC.CollectionCount(1);
+ var gen2After = GC.CollectionCount(2);
// Get process memory info
var process = System.Diagnostics.Process.GetCurrentProcess();
return Ok(new {
Timestamp = DateTime.UtcNow,
- GCMemoryBytes = memoryUsage,
- GCMemoryMB = Math.Round(memoryUsage / (1024.0 * 1024.0), 2),
+ BeforeGC = new {
+ GCMemoryBytes = memoryBeforeGC,
+ GCMemoryMB = Math.Round(memoryBeforeGC / (1024.0 * 1024.0), 2)
+ },
+ AfterGC = new {
+ GCMemoryBytes = memoryAfterGC,
+ GCMemoryMB = Math.Round(memoryAfterGC / (1024.0 * 1024.0), 2)
+ },
+ MemoryFreedMB = Math.Round((memoryBeforeGC - memoryAfterGC) / (1024.0 * 1024.0), 2),
ProcessWorkingSetBytes = process.WorkingSet64,
ProcessWorkingSetMB = Math.Round(process.WorkingSet64 / (1024.0 * 1024.0), 2),
ProcessPrivateMemoryBytes = process.PrivateMemorySize64,
@@ -1935,9 +1948,15 @@ public class AdminController : ControllerBase
ProcessVirtualMemoryBytes = process.VirtualMemorySize64,
ProcessVirtualMemoryMB = Math.Round(process.VirtualMemorySize64 / (1024.0 * 1024.0), 2),
GCCollections = new {
- Gen0 = gen0,
- Gen1 = gen1,
- Gen2 = gen2
+ Gen0Before = gen0Before,
+ Gen0After = gen0After,
+ Gen0Triggered = gen0After - gen0Before,
+ Gen1Before = gen1Before,
+ Gen1After = gen1After,
+ Gen1Triggered = gen1After - gen1Before,
+ Gen2Before = gen2Before,
+ Gen2After = gen2After,
+ Gen2Triggered = gen2After - gen2Before
},
GCMode = GCSettings.IsServerGC ? "Server" : "Workstation",
GCLatencyMode = GCSettings.LatencyMode.ToString()
@@ -1948,6 +1967,54 @@ public class AdminController : ControllerBase
return BadRequest(new { error = ex.Message });
}
}
+
+ ///
+ /// Forces garbage collection to free up memory (emergency use only).
+ ///
+ [HttpPost("force-gc")]
+ public IActionResult ForceGarbageCollection()
+ {
+ try
+ {
+ var memoryBefore = GC.GetTotalMemory(false);
+ var processBefore = System.Diagnostics.Process.GetCurrentProcess().WorkingSet64;
+
+ // Force full garbage collection
+ GC.Collect(2, GCCollectionMode.Forced);
+ GC.WaitForPendingFinalizers();
+ GC.Collect(2, GCCollectionMode.Forced);
+
+ var memoryAfter = GC.GetTotalMemory(false);
+ var processAfter = System.Diagnostics.Process.GetCurrentProcess().WorkingSet64;
+
+ return Ok(new {
+ Timestamp = DateTime.UtcNow,
+ MemoryFreedMB = Math.Round((memoryBefore - memoryAfter) / (1024.0 * 1024.0), 2),
+ ProcessMemoryFreedMB = Math.Round((processBefore - processAfter) / (1024.0 * 1024.0), 2),
+ BeforeGCMB = Math.Round(memoryBefore / (1024.0 * 1024.0), 2),
+ AfterGCMB = Math.Round(memoryAfter / (1024.0 * 1024.0), 2),
+ BeforeProcessMB = Math.Round(processBefore / (1024.0 * 1024.0), 2),
+ AfterProcessMB = Math.Round(processAfter / (1024.0 * 1024.0), 2)
+ });
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Helper method to trigger GC after large file operations to prevent memory leaks.
+ ///
+ private static void TriggerGCAfterLargeOperation(int sizeInBytes)
+ {
+ // Only trigger GC for files larger than 1MB to avoid performance impact
+ if (sizeInBytes > 1024 * 1024)
+ {
+ // Suggest GC collection for large objects (they go to LOH and aren't collected as frequently)
+ GC.Collect(2, GCCollectionMode.Optimized, blocking: false);
+ }
+ }
}
public class ConfigUpdateRequest
diff --git a/allstarr/Services/Jellyfin/JellyfinProxyService.cs b/allstarr/Services/Jellyfin/JellyfinProxyService.cs
index 8547d95..187a7a8 100644
--- a/allstarr/Services/Jellyfin/JellyfinProxyService.cs
+++ b/allstarr/Services/Jellyfin/JellyfinProxyService.cs
@@ -409,6 +409,7 @@ public class JellyfinProxyService
///
/// Sends a GET request and returns raw bytes (for images, audio streams).
+ /// WARNING: This loads entire response into memory - use StreamAsync for large files!
///
public async Task<(byte[] Body, string? ContentType)> GetBytesAsync(string endpoint, Dictionary? queryParams = null)
{
@@ -423,9 +424,35 @@ public class JellyfinProxyService
var body = await response.Content.ReadAsByteArrayAsync();
var contentType = response.Content.Headers.ContentType?.ToString();
+ // Trigger GC for large files to prevent memory leaks
+ if (body.Length > 1024 * 1024) // 1MB threshold
+ {
+ GC.Collect(2, GCCollectionMode.Optimized, blocking: false);
+ }
+
return (body, contentType);
}
+ ///
+ /// Streams content directly without loading into memory (for large files like audio).
+ ///
+ public async Task<(Stream Stream, string? ContentType, long? ContentLength)> GetStreamAsync(string endpoint, Dictionary? queryParams = null)
+ {
+ var url = BuildUrl(endpoint, queryParams);
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, url);
+ request.Headers.Add("Authorization", GetAuthorizationHeader());
+
+ var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
+ response.EnsureSuccessStatusCode();
+
+ var stream = await response.Content.ReadAsStreamAsync();
+ var contentType = response.Content.Headers.ContentType?.ToString();
+ var contentLength = response.Content.Headers.ContentLength;
+
+ return (stream, contentType, contentLength);
+ }
+
///
/// Sends a DELETE request to the Jellyfin server.
/// Forwards client headers for authentication passthrough.
diff --git a/allstarr/Services/Subsonic/SubsonicProxyService.cs b/allstarr/Services/Subsonic/SubsonicProxyService.cs
index 34a9a31..3bd3cba 100644
--- a/allstarr/Services/Subsonic/SubsonicProxyService.cs
+++ b/allstarr/Services/Subsonic/SubsonicProxyService.cs
@@ -39,6 +39,12 @@ public class SubsonicProxyService
var body = await response.Content.ReadAsByteArrayAsync();
var contentType = response.Content.Headers.ContentType?.ToString();
+ // Trigger GC for large files to prevent memory leaks
+ if (body.Length > 1024 * 1024) // 1MB threshold
+ {
+ GC.Collect(2, GCCollectionMode.Optimized, blocking: false);
+ }
+
return (body, contentType);
}