Skip to content

Commit

Permalink
fix: do not reuse Playwright session as this leaks memory
Browse files Browse the repository at this point in the history
  • Loading branch information
mu88 committed Jul 17, 2024
1 parent 2c9ca36 commit 0b4592f
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 62 deletions.
8 changes: 8 additions & 0 deletions src/ScreenshotCreator.Logic/IPlaywrightFacade.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Playwright;

namespace ScreenshotCreator.Logic;

public interface IPlaywrightFacade : IAsyncDisposable
{
ValueTask<IPage> GetPlaywrightPageAsync();
}
6 changes: 2 additions & 4 deletions src/ScreenshotCreator.Logic/IPlaywrightHelper.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using Microsoft.Playwright;

namespace ScreenshotCreator.Logic;
namespace ScreenshotCreator.Logic;

public interface IPlaywrightHelper
{
ValueTask<IPage> InitializePlaywrightAsync();
IPlaywrightFacade CreatePlaywrightFacade();

Task WaitAsync();
}
30 changes: 30 additions & 0 deletions src/ScreenshotCreator.Logic/PlaywrightFacade.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.Playwright;

namespace ScreenshotCreator.Logic;

internal class PlaywrightFacade : IPlaywrightFacade
{
private IBrowser? _browser;
private bool _disposed;
private IPage? _page;
private IPlaywright? _playwright;

public async ValueTask DisposeAsync()
{
if (_disposed) return;

if (_browser != null) await _browser.DisposeAsync();
_playwright?.Dispose();

_disposed = true;
}

public async ValueTask<IPage> GetPlaywrightPageAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync();
_page = await _browser.NewPageAsync(new BrowserNewPageOptions { TimezoneId = Environment.GetEnvironmentVariable("TZ") });

return _page;
}
}
39 changes: 3 additions & 36 deletions src/ScreenshotCreator.Logic/PlaywrightHelper.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Playwright;
using Microsoft.Extensions.Options;

namespace ScreenshotCreator.Logic;

[ExcludeFromCodeCoverage]
public sealed class PlaywrightHelper(IOptions<ScreenshotOptions> options, ILogger<PlaywrightHelper> logger) : IPlaywrightHelper, IAsyncDisposable
public class PlaywrightHelper(IOptions<ScreenshotOptions> options) : IPlaywrightHelper
{
private readonly ScreenshotOptions _screenshotOptions = options.Value;
private IPlaywright? _playwright;
private IBrowser? _browser;
private IPage? _page;
private bool _disposed;

public async ValueTask DisposeAsync()
{
if (_disposed) return;

if (_browser != null) await _browser.DisposeAsync();
_playwright?.Dispose();

_disposed = true;
}

/// <inheritdoc />
public async ValueTask<IPage> InitializePlaywrightAsync()
{
if (!_screenshotOptions.DoNotReusePlaywrightPage && _page != null)
{
logger.ReusingPlaywrightPage();
return _page;
}

_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync();
_page = await _browser.NewPageAsync(new BrowserNewPageOptions { TimezoneId = Environment.GetEnvironmentVariable("TZ") });

logger.PlaywrightInitialized();

return _page;
}
public IPlaywrightFacade CreatePlaywrightFacade() => new PlaywrightFacade();

/// <inheritdoc />
public async Task WaitAsync() => await Task.Delay(TimeSpan.FromSeconds(_screenshotOptions.TimeBetweenHttpCallsInSeconds));
Expand Down
3 changes: 3 additions & 0 deletions src/ScreenshotCreator.Logic/ScreenshotCreator.Logic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2"/>
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Tests"/>
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion src/ScreenshotCreator.Logic/ScreenshotCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public sealed class ScreenshotCreator(IPlaywrightHelper playwrightHelper, IOptio

public async Task CreateScreenshotAsync(uint width, uint height)
{
var page = await playwrightHelper.InitializePlaywrightAsync();
await using var playwrightFacade = playwrightHelper.CreatePlaywrightFacade();
var page = await playwrightFacade.GetPlaywrightPageAsync();

await page.SetViewportSizeAsync((int)width, (int)height);
if (await NeedsLoginAsync(page)) { await LoginAsync(page); }
Expand Down
4 changes: 1 addition & 3 deletions src/ScreenshotCreator.Logic/ScreenshotOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ public class ScreenshotOptions
[Required]
public bool BackgroundProcessingEnabled { get; set; }

public bool BackgroundProcessingWithTryCatch { get; set; } = false;

public bool DoNotReusePlaywrightPage { get; set; } = false;
public bool BackgroundProcessingWithTryCatch { get; set; }

public Activity? Activity { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion tests/Tests/Integration/Api/ProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public async Task CreateImageNowForOpenHab()
result.Should().HaveStatusCode(HttpStatusCode.OK);
result.Content.Headers.ContentType.Should().NotBeNull();
result.Content.Headers.ContentType!.MediaType.Should().Be("image/png");
(await result.Content.ReadAsByteArrayAsync()).Length.Should().BeInRange(8000, 15000);
(await result.Content.ReadAsByteArrayAsync()).Length.Should().BeInRange(7000, 15000);
}

[Test]
Expand Down
37 changes: 37 additions & 0 deletions tests/Tests/Integration/Logic/PlaywrightFacadeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using FluentAssertions;
using ScreenshotCreator.Logic;

namespace Tests.Integration.Logic;

[TestFixture]
[Category("Integration")]
public class PlaywrightFacadeTests : PlaywrightTests
{
[Test]
public async Task GetPlaywrightPage_ShouldInitializePlaywrightAndCreateNewPage()
{
// Arrange
var testee = new PlaywrightFacade();

// Act
var result = await testee.GetPlaywrightPageAsync();

// Assert
result.Context.Browser.Should().NotBeNull();
}

[Test]
public async Task Dispose_ShouldDisposeUnderlyingPlaywrightSession()
{
// Arrange
var testee = new PlaywrightFacade();
var result = await testee.GetPlaywrightPageAsync();

// Act & Assert
await testee.DisposeAsync();

// Assert
var createScreenShotAsync = async () => await result.ScreenshotAsync();
await createScreenShotAsync.Should().ThrowAsync<Exception>().WithMessage("*disposed*");
}
}
40 changes: 40 additions & 0 deletions tests/Tests/Integration/Logic/PlaywrightHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Diagnostics;
using FluentAssertions;
using Microsoft.Extensions.Options;
using ScreenshotCreator.Logic;

namespace Tests.Integration.Logic;

[TestFixture]
[Category("Integration")]
public class PlaywrightHelperTests : PlaywrightTests
{
[Test]
public void CreatePlaywrightFacade_ShouldCreateNewObjectEveryTime()
{
// Arrange
var testee = new PlaywrightHelper(Options.Create(new ScreenshotOptions()));

// Act
var result1 = testee.CreatePlaywrightFacade();
var result2 = testee.CreatePlaywrightFacade();

// Assert
result1.Should().NotBeSameAs(result2);
}

[Test]
public async Task Wait_ShouldWaitTheConfiguredAmountOfSeconds()
{
// Arrange
var testee = new PlaywrightHelper(Options.Create(new ScreenshotOptions { TimeBetweenHttpCallsInSeconds = 1 }));

// Act
var stopwatch = Stopwatch.StartNew();
await testee.WaitAsync();
stopwatch.Stop();

// Assert
stopwatch.Elapsed.Should().BeCloseTo(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(100));
}
}
51 changes: 34 additions & 17 deletions tests/Tests/Unit/Logic/ScreenshotCreatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ public async Task CreateScreenshotWithoutLoginByConfig(string subresource)
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = $"https://www.mysite.com{subresource}", UrlType = UrlType.Any };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
NullLogger<ScreenshotCreator.Logic.ScreenshotCreator>.Instance);
Expand All @@ -33,8 +35,9 @@ public async Task CreateScreenshotWithoutLoginByConfig(string subresource)
await pageMock.Received(1)
.ScreenshotAsync(Arg.Is<PageScreenshotOptions>(options => options.Path == screenshotOptions.ScreenshotFile &&
options.Type == ScreenshotType.Png));
await playwrightHelperMock.Received(1).InitializePlaywrightAsync();
playwrightHelperMock.Received(1).CreatePlaywrightFacade();
await playwrightHelperMock.Received(1).WaitAsync();
await playwrightFacadeMock.Received(1).GetPlaywrightPageAsync();
}

[TestCase("")]
Expand All @@ -43,10 +46,12 @@ public async Task CreateScreenshotWithoutLoginByPageContent(string subresource)
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = $"https://www.mysite.com{subresource}", UrlType = UrlType.OpenHab };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
pageMock.GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync().Returns(0);
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
NullLogger<ScreenshotCreator.Logic.ScreenshotCreator>.Instance);
Expand All @@ -60,8 +65,9 @@ public async Task CreateScreenshotWithoutLoginByPageContent(string subresource)
await pageMock.Received(1)
.ScreenshotAsync(Arg.Is<PageScreenshotOptions>(options => options.Path == screenshotOptions.ScreenshotFile &&
options.Type == ScreenshotType.Png));
await playwrightHelperMock.Received(1).InitializePlaywrightAsync();
playwrightHelperMock.Received(1).CreatePlaywrightFacade();
await playwrightHelperMock.Received(2).WaitAsync();
await playwrightFacadeMock.Received(1).GetPlaywrightPageAsync();
}

[TestCase("", 3)]
Expand All @@ -70,11 +76,13 @@ public async Task CreateScreenshotWithLogin(string subresource, int expectedCall
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = $"https://www.mysite.com{subresource}", UrlType = UrlType.OpenHab };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
pageMock.GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync().Returns(1);
pageMock.GetByText("lock_shield_fill").IsVisibleAsync().Returns(true);
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
Substitute.For<ILogger<ScreenshotCreator.Logic.ScreenshotCreator>>());
Expand All @@ -94,21 +102,24 @@ await pageMock.Received(1)
.ScreenshotAsync(Arg.Is<PageScreenshotOptions>(options => options.Path == screenshotOptions.ScreenshotFile &&
options.Type == ScreenshotType.Png));
await pageMock.Received(2).GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync();
await playwrightHelperMock.Received(1).InitializePlaywrightAsync();
playwrightHelperMock.Received(1).CreatePlaywrightFacade();
await playwrightHelperMock.Received(4).WaitAsync();
await playwrightFacadeMock.Received(1).GetPlaywrightPageAsync();
}

[Test]
public async Task CreateScreenshotWithLogin_ShouldOpenMenuAndClickLogin_IfLoginPageIsNotShown()
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = "https://www.mysite.com", UrlType = UrlType.OpenHab };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
pageMock.GetByPlaceholder("User Name").IsVisibleAsync().Returns(false);
pageMock.GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync().Returns(1);
pageMock.GetByText("lock_shield_fill").IsVisibleAsync().Returns(false);
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
Substitute.For<ILogger<ScreenshotCreator.Logic.ScreenshotCreator>>());
Expand All @@ -130,12 +141,14 @@ public async Task CreateScreenshotWithLogin_ShouldNotOpenMenuAndLogin_IfLoginPag
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = "https://www.mysite.com", UrlType = UrlType.OpenHab };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
pageMock.GetByPlaceholder("User Name").IsVisibleAsync().Returns(true);
pageMock.GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync().Returns(1);
pageMock.GetByText("lock_shield_fill").IsVisibleAsync().Returns(false);
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
Substitute.For<ILogger<ScreenshotCreator.Logic.ScreenshotCreator>>());
Expand All @@ -156,12 +169,14 @@ public async Task CreateScreenshotWithLogin_ShouldNotCreateScreenshot_IfSiteDoes
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = "https://www.mysite.com", UrlType = UrlType.OpenHab, AvailabilityIndicator = "Success" };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
pageMock.GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync().Returns(1);
pageMock.GetByText(screenshotOptions.AvailabilityIndicator).CountAsync().Returns(0);
pageMock.GetByText("lock_shield_fill").IsVisibleAsync().Returns(true);
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
Substitute.For<ILogger<ScreenshotCreator.Logic.ScreenshotCreator>>());
Expand All @@ -180,12 +195,14 @@ public async Task CreateScreenshotWithLogin_ShouldCreateScreenshot_IfSiteIndicat
{
// Arrange
var screenshotOptions = new ScreenshotOptions { Url = "https://www.mysite.com", UrlType = UrlType.OpenHab, AvailabilityIndicator = "Success" };
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
var pageMock = Substitute.For<IPage>();
pageMock.GetByText("You are not allowed to view this page because of visibility restrictions.").CountAsync().Returns(1);
pageMock.GetByText(screenshotOptions.AvailabilityIndicator).CountAsync().Returns(1);
pageMock.GetByText("lock_shield_fill").IsVisibleAsync().Returns(true);
playwrightHelperMock.InitializePlaywrightAsync().Returns(pageMock);
var playwrightFacadeMock = Substitute.For<IPlaywrightFacade>();
playwrightFacadeMock.GetPlaywrightPageAsync().Returns(pageMock);
var playwrightHelperMock = Substitute.For<IPlaywrightHelper>();
playwrightHelperMock.CreatePlaywrightFacade().Returns(playwrightFacadeMock);
var testee = new ScreenshotCreator.Logic.ScreenshotCreator(playwrightHelperMock,
Options.Create(screenshotOptions),
Substitute.For<ILogger<ScreenshotCreator.Logic.ScreenshotCreator>>());
Expand Down

0 comments on commit 0b4592f

Please sign in to comment.