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(); } } } }