diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 2f17604ea9e5b..4d252112cdf04 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2333,10 +2333,57 @@ last redirect. If cannot go forward, returns `null`. Navigate to the next page in history. -## async method: Page.forceGarbageCollection -* since: v1.47 +## async method: Page.requestGC +* since: v1.48 + +Request the page to perform garbage collection. Note that there is no guarantee that all unreachable objects will be collected. + +This is useful to help detect memory leaks. For example, if your page has a large object `'suspect'` that might be leaked, you can check that it does not leak by using a [`WeakRef`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef). + +```js +// 1. In your page, save a WeakRef for the "suspect". +await page.evaluate(() => globalThis.suspectWeakRef = new WeakRef(suspect)); +// 2. Request garbage collection. +await page.requestGC(); +// 3. Check that weak ref does not deref to the original object. +expect(await page.evaluate(() => !globalThis.suspectWeakRef.deref())).toBe(true); +``` + +```java +// 1. In your page, save a WeakRef for the "suspect". +page.evaluate("globalThis.suspectWeakRef = new WeakRef(suspect)"); +// 2. Request garbage collection. +page.requestGC(); +// 3. Check that weak ref does not deref to the original object. +assertTrue(page.evaluate("!globalThis.suspectWeakRef.deref()")); +``` -Force the browser to perform garbage collection. +```python async +# 1. In your page, save a WeakRef for the "suspect". +await page.evaluate("globalThis.suspectWeakRef = new WeakRef(suspect)") +# 2. Request garbage collection. +await page.request_gc() +# 3. Check that weak ref does not deref to the original object. +assert await page.evaluate("!globalThis.suspectWeakRef.deref()") +``` + +```python sync +# 1. In your page, save a WeakRef for the "suspect". +page.evaluate("globalThis.suspectWeakRef = new WeakRef(suspect)") +# 2. Request garbage collection. +page.request_gc() +# 3. Check that weak ref does not deref to the original object. +assert page.evaluate("!globalThis.suspectWeakRef.deref()") +``` + +```csharp +// 1. In your page, save a WeakRef for the "suspect". +await Page.EvaluateAsync("globalThis.suspectWeakRef = new WeakRef(suspect)"); +// 2. Request garbage collection. +await Page.RequestGCAsync(); +// 3. Check that weak ref does not deref to the original object. +Assert.True(await Page.EvaluateAsync("!globalThis.suspectWeakRef.deref()")); +``` ### option: Page.goForward.waitUntil = %%-navigation-wait-until-%% * since: v1.8 diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 66842cad0b2a8..6654294edd3e4 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -478,8 +478,8 @@ export class Page extends ChannelOwner implements api.Page return Response.fromNullable((await this._channel.goForward({ ...options, waitUntil })).response); } - async forceGarbageCollection() { - await this._channel.forceGarbageCollection(); + async requestGC() { + await this._channel.requestGC(); } async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null } = {}) { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 9b36ad8883fe4..4069b906c76bc 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1137,8 +1137,8 @@ scheme.PageGoForwardParams = tObject({ scheme.PageGoForwardResult = tObject({ response: tOptional(tChannel(['Response'])), }); -scheme.PageForceGarbageCollectionParams = tOptional(tObject({})); -scheme.PageForceGarbageCollectionResult = tOptional(tObject({})); +scheme.PageRequestGCParams = tOptional(tObject({})); +scheme.PageRequestGCResult = tOptional(tObject({})); scheme.PageRegisterLocatorHandlerParams = tObject({ selector: tString, noWaitAfter: tOptional(tBoolean), diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index a50217975ae1a..56bb43cb1ac0b 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -340,7 +340,7 @@ export class BidiPage implements PageDelegate { }).then(() => true).catch(() => false); } - async forceGarbageCollection(): Promise { + async requestGC(): Promise { throw new Error('Method not implemented.'); } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index bba14ff00eeb7..c1ff8033388e7 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -247,7 +247,7 @@ export class CRPage implements PageDelegate { return this._go(+1); } - async forceGarbageCollection(): Promise { + async requestGC(): Promise { await this._mainFrameSession._client.send('HeapProfiler.collectGarbage'); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 265e37bd459c2..5e3b73a73f024 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -139,8 +139,8 @@ export class PageDispatcher extends Dispatcher { - await this._page.forceGarbageCollection(); + async requestGC(params: channels.PageRequestGCParams, metadata: CallMetadata): Promise { + await this._page.requestGC(); } async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise { diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 9dfffc1eb3384..61790feae42c5 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -399,7 +399,7 @@ export class FFPage implements PageDelegate { return success; } - async forceGarbageCollection(): Promise { + async requestGC(): Promise { await this._session.send('Heap.collectGarbage'); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 04728b681e4e9..3fb72bb0c3d5b 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -54,7 +54,7 @@ export interface PageDelegate { reload(): Promise; goBack(): Promise; goForward(): Promise; - forceGarbageCollection(): Promise; + requestGC(): Promise; addInitScript(initScript: InitScript): Promise; removeNonInternalInitScripts(): Promise; closePage(runBeforeUnload: boolean): Promise; @@ -431,8 +431,8 @@ export class Page extends SdkObject { }), this._timeoutSettings.navigationTimeout(options)); } - forceGarbageCollection(): Promise { - return this._delegate.forceGarbageCollection(); + requestGC(): Promise { + return this._delegate.requestGC(); } registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) { diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 320df04ce27e4..3ba773241a900 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -767,7 +767,7 @@ export class WKPage implements PageDelegate { }); } - async forceGarbageCollection(): Promise { + async requestGC(): Promise { await this._session.send('Heap.gc'); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 30eed649ad505..eacc1393e349c 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2558,11 +2558,6 @@ export interface Page { timeout?: number; }): Promise; - /** - * Force the browser to perform garbage collection. - */ - forceGarbageCollection(): Promise; - /** * Returns frame matching the specified criteria. Either `name` or `url` must be specified. * @@ -3712,6 +3707,26 @@ export interface Page { */ removeLocatorHandler(locator: Locator): Promise; + /** + * Request the page to perform garbage collection. Note that there is no guarantee that all unreachable objects will + * be collected. + * + * This is useful to help detect memory leaks. For example, if your page has a large object `'suspect'` that might be + * leaked, you can check that it does not leak by using a + * [`WeakRef`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef). + * + * ```js + * // 1. In your page, save a WeakRef for the "suspect". + * await page.evaluate(() => globalThis.suspectWeakRef = new WeakRef(suspect)); + * // 2. Request garbage collection. + * await page.requestGC(); + * // 3. Check that weak ref does not deref to the original object. + * expect(await page.evaluate(() => !globalThis.suspectWeakRef.deref())).toBe(true); + * ``` + * + */ + requestGC(): Promise; + /** * Routing provides the capability to modify network requests that are made by a page. * diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 66cf417c87cee..b99490a95565b 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1956,7 +1956,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel { exposeBinding(params: PageExposeBindingParams, metadata?: CallMetadata): Promise; goBack(params: PageGoBackParams, metadata?: CallMetadata): Promise; goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise; - forceGarbageCollection(params?: PageForceGarbageCollectionParams, metadata?: CallMetadata): Promise; + requestGC(params?: PageRequestGCParams, metadata?: CallMetadata): Promise; registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise; resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise; unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise; @@ -2098,9 +2098,9 @@ export type PageGoForwardOptions = { export type PageGoForwardResult = { response?: ResponseChannel, }; -export type PageForceGarbageCollectionParams = {}; -export type PageForceGarbageCollectionOptions = {}; -export type PageForceGarbageCollectionResult = void; +export type PageRequestGCParams = {}; +export type PageRequestGCOptions = {}; +export type PageRequestGCResult = void; export type PageRegisterLocatorHandlerParams = { selector: string, noWaitAfter?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 5133c0672eccf..e9891adf6bd05 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1450,7 +1450,7 @@ Page: slowMo: true snapshot: true - forceGarbageCollection: + requestGC: registerLocatorHandler: parameters: diff --git a/tests/page/page-force-gc.spec.ts b/tests/page/page-request-gc.spec.ts similarity index 74% rename from tests/page/page-force-gc.spec.ts rename to tests/page/page-request-gc.spec.ts index 038d471eba65b..e39c79a42f47b 100644 --- a/tests/page/page-force-gc.spec.ts +++ b/tests/page/page-request-gc.spec.ts @@ -18,10 +18,17 @@ import { test, expect } from './pageTest'; test('should work', async ({ page }) => { await page.evaluate(() => { - globalThis.objectToDestroy = {}; + globalThis.objectToDestroy = { hello: 'world' }; globalThis.weakRef = new WeakRef(globalThis.objectToDestroy); }); + + await page.requestGC(); + expect(await page.evaluate(() => globalThis.weakRef.deref())).toEqual({ hello: 'world' }); + + await page.requestGC(); + expect(await page.evaluate(() => globalThis.weakRef.deref())).toEqual({ hello: 'world' }); + await page.evaluate(() => globalThis.objectToDestroy = null); - await page.forceGarbageCollection(); + await page.requestGC(); expect(await page.evaluate(() => globalThis.weakRef.deref())).toBe(undefined); });