using PuppeteerSharp;
namespace GB5Shared.Export.HybridReport
{
public static class GlobalBrowser
{
private static IBrowser? _browser;
private static readonly SemaphoreSlim _sem = new(1, 1);
// Bounds how many Chromium pages render concurrently against the one shared browser
// instance, across every export request on this process (chunked PDF exports fire many
// page renders at once; without this cap they'd all pile onto the browser unbounded).
// GetAsync()'s _sem above only serializes the one-time browser LAUNCH, not page usage.
//
// Deliberately NOT scaled to Environment.ProcessorCount — PDF printing in headless
// Chromium is memory-bound, not CPU-bound, and a high core-count server launching that
// many concurrent pages for a large report (e.g. a full fiscal year's worth of rows)
// exhausts Chromium's renderer memory and fails with "Protocol error (Page.printToPDF):
// Printing failed". A small fixed cap is the safe default regardless of machine size.
//
// Call Configure(int) once at Program.cs startup to override the default from config
// (PDFEXPORT:RenderConcurrency). Must be called before the first GetAsync().
public static SemaphoreSlim RenderGate { get; private set; } = new(2, 2);
///
/// Override the default render concurrency (2). Call once at app startup before the
/// first PDF export request arrives — e.g. from Program.cs reading PDFEXPORT:RenderConcurrency.
///
public static void Configure(int concurrency)
{
if (concurrency < 1) concurrency = 1;
RenderGate = new SemaphoreSlim(concurrency, concurrency);
}
public static async Task GetAsync()
{
if (_browser != null && _browser.IsConnected) return _browser;
await _sem.WaitAsync().ConfigureAwait(false);
try
{
if (_browser == null || !_browser.IsConnected)
{
var executablePath = await ChromiumResolver.GetExecutablePathAsync().ConfigureAwait(false);
_browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true,
ExecutablePath = executablePath,
Args = ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"]
}).ConfigureAwait(false);
}
}
finally
{
_sem.Release();
}
return _browser!;
}
///
/// Close and discard the current Chromium browser singleton so the next GetAsync() call
/// launches a fresh process. Call this in the retry loop after a "Protocol error
/// (Page.printToPDF): Printing failed" to recover from a corrupt renderer state.
///
public static async Task ResetAsync()
{
await _sem.WaitAsync().ConfigureAwait(false);
try
{
if (_browser != null)
{
try { await _browser.CloseAsync().ConfigureAwait(false); } catch { }
_browser = null;
}
}
finally
{
_sem.Release();
}
}
}
}