mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 07:58:39 -05:00
Add GC hints to prevent memory leaks from large byte arrays
This commit is contained in:
@@ -1911,23 +1911,36 @@ public class AdminController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
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
|
// Force garbage collection to get accurate numbers
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
GC.WaitForPendingFinalizers();
|
GC.WaitForPendingFinalizers();
|
||||||
GC.Collect();
|
GC.Collect();
|
||||||
|
|
||||||
var memoryUsage = GC.GetTotalMemory(false);
|
var memoryAfterGC = GC.GetTotalMemory(false);
|
||||||
var gen0 = GC.CollectionCount(0);
|
var gen0After = GC.CollectionCount(0);
|
||||||
var gen1 = GC.CollectionCount(1);
|
var gen1After = GC.CollectionCount(1);
|
||||||
var gen2 = GC.CollectionCount(2);
|
var gen2After = GC.CollectionCount(2);
|
||||||
|
|
||||||
// Get process memory info
|
// Get process memory info
|
||||||
var process = System.Diagnostics.Process.GetCurrentProcess();
|
var process = System.Diagnostics.Process.GetCurrentProcess();
|
||||||
|
|
||||||
return Ok(new {
|
return Ok(new {
|
||||||
Timestamp = DateTime.UtcNow,
|
Timestamp = DateTime.UtcNow,
|
||||||
GCMemoryBytes = memoryUsage,
|
BeforeGC = new {
|
||||||
GCMemoryMB = Math.Round(memoryUsage / (1024.0 * 1024.0), 2),
|
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,
|
ProcessWorkingSetBytes = process.WorkingSet64,
|
||||||
ProcessWorkingSetMB = Math.Round(process.WorkingSet64 / (1024.0 * 1024.0), 2),
|
ProcessWorkingSetMB = Math.Round(process.WorkingSet64 / (1024.0 * 1024.0), 2),
|
||||||
ProcessPrivateMemoryBytes = process.PrivateMemorySize64,
|
ProcessPrivateMemoryBytes = process.PrivateMemorySize64,
|
||||||
@@ -1935,9 +1948,15 @@ public class AdminController : ControllerBase
|
|||||||
ProcessVirtualMemoryBytes = process.VirtualMemorySize64,
|
ProcessVirtualMemoryBytes = process.VirtualMemorySize64,
|
||||||
ProcessVirtualMemoryMB = Math.Round(process.VirtualMemorySize64 / (1024.0 * 1024.0), 2),
|
ProcessVirtualMemoryMB = Math.Round(process.VirtualMemorySize64 / (1024.0 * 1024.0), 2),
|
||||||
GCCollections = new {
|
GCCollections = new {
|
||||||
Gen0 = gen0,
|
Gen0Before = gen0Before,
|
||||||
Gen1 = gen1,
|
Gen0After = gen0After,
|
||||||
Gen2 = gen2
|
Gen0Triggered = gen0After - gen0Before,
|
||||||
|
Gen1Before = gen1Before,
|
||||||
|
Gen1After = gen1After,
|
||||||
|
Gen1Triggered = gen1After - gen1Before,
|
||||||
|
Gen2Before = gen2Before,
|
||||||
|
Gen2After = gen2After,
|
||||||
|
Gen2Triggered = gen2After - gen2Before
|
||||||
},
|
},
|
||||||
GCMode = GCSettings.IsServerGC ? "Server" : "Workstation",
|
GCMode = GCSettings.IsServerGC ? "Server" : "Workstation",
|
||||||
GCLatencyMode = GCSettings.LatencyMode.ToString()
|
GCLatencyMode = GCSettings.LatencyMode.ToString()
|
||||||
@@ -1948,6 +1967,54 @@ public class AdminController : ControllerBase
|
|||||||
return BadRequest(new { error = ex.Message });
|
return BadRequest(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces garbage collection to free up memory (emergency use only).
|
||||||
|
/// </summary>
|
||||||
|
[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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to trigger GC after large file operations to prevent memory leaks.
|
||||||
|
/// </summary>
|
||||||
|
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
|
public class ConfigUpdateRequest
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ public class JellyfinProxyService
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a GET request and returns raw bytes (for images, audio streams).
|
/// Sends a GET request and returns raw bytes (for images, audio streams).
|
||||||
|
/// WARNING: This loads entire response into memory - use StreamAsync for large files!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<(byte[] Body, string? ContentType)> GetBytesAsync(string endpoint, Dictionary<string, string>? queryParams = null)
|
public async Task<(byte[] Body, string? ContentType)> GetBytesAsync(string endpoint, Dictionary<string, string>? queryParams = null)
|
||||||
{
|
{
|
||||||
@@ -423,9 +424,35 @@ public class JellyfinProxyService
|
|||||||
var body = await response.Content.ReadAsByteArrayAsync();
|
var body = await response.Content.ReadAsByteArrayAsync();
|
||||||
var contentType = response.Content.Headers.ContentType?.ToString();
|
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);
|
return (body, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams content directly without loading into memory (for large files like audio).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(Stream Stream, string? ContentType, long? ContentLength)> GetStreamAsync(string endpoint, Dictionary<string, string>? 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a DELETE request to the Jellyfin server.
|
/// Sends a DELETE request to the Jellyfin server.
|
||||||
/// Forwards client headers for authentication passthrough.
|
/// Forwards client headers for authentication passthrough.
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ public class SubsonicProxyService
|
|||||||
var body = await response.Content.ReadAsByteArrayAsync();
|
var body = await response.Content.ReadAsByteArrayAsync();
|
||||||
var contentType = response.Content.Headers.ContentType?.ToString();
|
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);
|
return (body, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user