|
1 | | -using Azure; |
| 1 | +using System.Collections.Concurrent; |
2 | 2 | using PuppeteerSharp; |
3 | 3 | using PWABuilder.Common; |
4 | | -using PWABuilder.Models; |
5 | 4 |
|
6 | 5 | namespace PWABuilder.Services |
7 | 6 | { |
8 | 7 | public class PuppeteerService : IPuppeteerService |
9 | 8 | { |
10 | 9 | private readonly Task<IBrowser> reusableBrowser; |
11 | 10 | private readonly ILogger<PuppeteerService> logger; |
| 11 | + private readonly ConcurrentBag<PageReference> openPages = []; |
12 | 12 |
|
13 | 13 | public PuppeteerService(Task<IBrowser> reusableBrowser, ILogger<PuppeteerService> logger) |
14 | 14 | { |
@@ -101,24 +101,12 @@ public async Task<IPage> NavigateAsync(Uri site) |
101 | 101 | { |
102 | 102 | logger.LogWarning("There are {pageCount} pages open in the Puppeteer browser. Possible page leak detected.", pages.Length); |
103 | 103 | } |
104 | | - if (pages.Length > 100) |
105 | | - { |
106 | | - // Extreme case: close all pages to free up resources. |
107 | | - logger.LogError("Too many pages ({pageCount}) open in the Puppeteer browser. Closing all pages to free up resources.", pages.Length); |
108 | | - foreach (var openPage in pages) |
109 | | - { |
110 | | - try |
111 | | - { |
112 | | - await openPage.CloseAsync(); |
113 | | - } |
114 | | - catch (Exception ex) |
115 | | - { |
116 | | - logger.LogError(ex, "Error closing page during cleanup."); |
117 | | - } |
118 | | - } |
119 | | - } |
| 104 | + |
| 105 | + // Close any pages that have been opened for more than an hour. |
| 106 | + await TryCleanupOldPages(); |
120 | 107 |
|
121 | 108 | var page = await browser.NewPageAsync(); |
| 109 | + this.openPages.Add(new PageReference(page, site)); |
122 | 110 |
|
123 | 111 | // Disable performance monitoring to avoid Protocol error (Performance.enable) issues |
124 | 112 | await page.SetCacheEnabledAsync(false); |
@@ -154,5 +142,47 @@ public async ValueTask DisposeAsync() |
154 | 142 | var browser = await this.reusableBrowser; |
155 | 143 | await browser.DisposeAsync(); |
156 | 144 | } |
| 145 | + |
| 146 | + private async Task TryCleanupOldPages() |
| 147 | + { |
| 148 | + var hourAgo = DateTimeOffset.UtcNow.AddHours(-1); |
| 149 | + foreach (var openPage in this.openPages) |
| 150 | + { |
| 151 | + if (openPage.CreatedAt < hourAgo) |
| 152 | + { |
| 153 | + logger.LogInformation("Closing stale Puppeteer page for {url} opened {time} ago.", openPage.Url, (DateTimeOffset.UtcNow - openPage.CreatedAt).ToString(@"hh\:mm")); |
| 154 | + openPage.Page.TryGetTarget(out var page); |
| 155 | + if (page != null) |
| 156 | + { |
| 157 | + openPages.TryTake(out var _); |
| 158 | + try |
| 159 | + { |
| 160 | + await page.CloseAsync(); |
| 161 | + } |
| 162 | + catch (Exception closeError) |
| 163 | + { |
| 164 | + logger.LogWarning(closeError, "Error closing stale Puppeteer page for {url}.", openPage.Url); |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + /// <summary> |
| 173 | + /// An open page in Puppeteer. Uses a weak reference to allow for garbage collection. Used for cleaning up old pages that weren't properly disposed. |
| 174 | + /// </summary> |
| 175 | + public class PageReference |
| 176 | + { |
| 177 | + public WeakReference<IPage> Page { get; } |
| 178 | + public DateTimeOffset CreatedAt { get; } |
| 179 | + public Uri Url { get; } |
| 180 | + |
| 181 | + public PageReference(IPage page, Uri url) |
| 182 | + { |
| 183 | + Page = new WeakReference<IPage>(page); |
| 184 | + CreatedAt = DateTimeOffset.UtcNow; |
| 185 | + Url = url; |
| 186 | + } |
157 | 187 | } |
158 | 188 | } |
0 commit comments