From f2a974b0451db21295a1dde2f8670483d8a5697b Mon Sep 17 00:00:00 2001 From: Matthew Jee Date: Fri, 13 Sep 2024 14:09:36 -0700 Subject: [PATCH] feat(api): add method to force garbage collection (#32383) --- .../firefox/juggler/protocol/PageHandler.js | 7 +++++ .../firefox/juggler/protocol/Protocol.js | 13 ++++++++- docs/src/api/class-page.md | 5 ++++ packages/playwright-core/src/client/page.ts | 4 +++ .../playwright-core/src/protocol/validator.ts | 2 ++ .../src/server/bidi/bidiPage.ts | 4 +++ .../src/server/chromium/crPage.ts | 4 +++ .../src/server/dispatchers/pageDispatcher.ts | 4 +++ .../src/server/firefox/ffPage.ts | 4 +++ packages/playwright-core/src/server/page.ts | 5 ++++ .../src/server/webkit/wkPage.ts | 4 +++ packages/playwright-core/types/types.d.ts | 5 ++++ packages/protocol/src/channels.ts | 4 +++ packages/protocol/src/protocol.yml | 2 ++ tests/page/page-force-gc.spec.ts | 27 +++++++++++++++++++ 15 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 tests/page/page-force-gc.spec.ts diff --git a/browser_patches/firefox/juggler/protocol/PageHandler.js b/browser_patches/firefox/juggler/protocol/PageHandler.js index 8fa9a06361..bab151b392 100644 --- a/browser_patches/firefox/juggler/protocol/PageHandler.js +++ b/browser_patches/firefox/juggler/protocol/PageHandler.js @@ -256,6 +256,13 @@ class PageHandler { return await this._contentPage.send('disposeObject', options); } + async ['Heap.collectGarbage']() { + Services.obs.notifyObservers(null, "child-gc-request"); + Cu.forceGC(); + Services.obs.notifyObservers(null, "child-cc-request"); + Cu.forceCC(); + } + async ['Network.getResponseBody']({requestId}) { return this._pageNetwork.getResponseBody(requestId); } diff --git a/browser_patches/firefox/juggler/protocol/Protocol.js b/browser_patches/firefox/juggler/protocol/Protocol.js index 6c9b700f05..2b7ad56d6a 100644 --- a/browser_patches/firefox/juggler/protocol/Protocol.js +++ b/browser_patches/firefox/juggler/protocol/Protocol.js @@ -487,6 +487,17 @@ const Browser = { }, }; +const Heap = { + targets: ['page'], + types: {}, + events: {}, + methods: { + 'collectGarbage': { + params: {}, + }, + }, +}; + const Network = { targets: ['page'], types: networkTypes, @@ -1002,7 +1013,7 @@ const Accessibility = { } this.protocol = { - domains: {Browser, Page, Runtime, Network, Accessibility}, + domains: {Browser, Heap, Page, Runtime, Network, Accessibility}, }; this.checkScheme = checkScheme; this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index e1aa908041..d36a96ee72 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2333,6 +2333,11 @@ last redirect. If cannot go forward, returns `null`. Navigate to the next page in history. +## async method: Page.forceGarbageCollection +* since: v1.47 + +Force the browser to perform garbage collection. + ### 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 a10286fa9a..0bbe78f4c8 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -468,6 +468,10 @@ 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 emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null } = {}) { await this._channel.emulateMedia({ media: options.media === null ? 'no-override' : options.media, diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index b67edcbca8..abea7f8fce 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1122,6 +1122,8 @@ scheme.PageGoForwardParams = tObject({ scheme.PageGoForwardResult = tObject({ response: tOptional(tChannel(['Response'])), }); +scheme.PageForceGarbageCollectionParams = tOptional(tObject({})); +scheme.PageForceGarbageCollectionResult = 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 f06924d70f..c2d499bd67 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -323,6 +323,10 @@ export class BidiPage implements PageDelegate { throw new Error('Method not implemented.'); } + async forceGarbageCollection(): Promise { + throw new Error('Method not implemented.'); + } + async addInitScript(initScript: InitScript): Promise { const { script } = await this._session.send('script.addPreloadScript', { // TODO: remove function call from the source. diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 5a7fb5e4af..fbdc9db91a 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -247,6 +247,10 @@ export class CRPage implements PageDelegate { return this._go(+1); } + async forceGarbageCollection(): Promise { + await this._mainFrameSession._client.send('HeapProfiler.collectGarbage'); + } + async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise { await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world)); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 3101cd051d..a97ddbf1f0 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -137,6 +137,10 @@ export class PageDispatcher extends Dispatcher { + await this._page.forceGarbageCollection(); + } + async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise { const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter); return { uid }; diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index d1066876eb..03a27954dd 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -400,6 +400,10 @@ export class FFPage implements PageDelegate { return success; } + async forceGarbageCollection(): Promise { + await this._session.send('Heap.collectGarbage'); + } + async addInitScript(initScript: InitScript, worldName?: string): Promise { this._initScripts.push({ initScript, worldName }); await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) }); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index f2e59aa56f..aeaeb0af88 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -54,6 +54,7 @@ export interface PageDelegate { reload(): Promise; goBack(): Promise; goForward(): Promise; + forceGarbageCollection(): Promise; addInitScript(initScript: InitScript): Promise; removeNonInternalInitScripts(): Promise; closePage(runBeforeUnload: boolean): Promise; @@ -430,6 +431,10 @@ export class Page extends SdkObject { }), this._timeoutSettings.navigationTimeout(options)); } + forceGarbageCollection(): Promise { + return this._delegate.forceGarbageCollection(); + } + registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) { const uid = ++this._lastLocatorHandlerUid; this._locatorHandlers.set(uid, { selector, noWaitAfter }); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index c3954b4882..2f579b619b 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -768,6 +768,10 @@ export class WKPage implements PageDelegate { }); } + async forceGarbageCollection(): Promise { + await this._session.send('Heap.gc'); + } + async addInitScript(initScript: InitScript): Promise { await this._updateBootstrapScript(); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 1c3b7f50b2..80d99a732b 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2554,6 +2554,11 @@ 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. * diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 143f1ad0e0..689f0275b1 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1933,6 +1933,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; registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise; resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise; unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise; @@ -2070,6 +2071,9 @@ export type PageGoForwardOptions = { export type PageGoForwardResult = { response?: ResponseChannel, }; +export type PageForceGarbageCollectionParams = {}; +export type PageForceGarbageCollectionOptions = {}; +export type PageForceGarbageCollectionResult = void; export type PageRegisterLocatorHandlerParams = { selector: string, noWaitAfter?: boolean, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4f064ffa08..ce206ab569 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1430,6 +1430,8 @@ Page: slowMo: true snapshot: true + forceGarbageCollection: + registerLocatorHandler: parameters: selector: string diff --git a/tests/page/page-force-gc.spec.ts b/tests/page/page-force-gc.spec.ts new file mode 100644 index 0000000000..038d471eba --- /dev/null +++ b/tests/page/page-force-gc.spec.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024 Adobe Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './pageTest'; + +test('should work', async ({ page }) => { + await page.evaluate(() => { + globalThis.objectToDestroy = {}; + globalThis.weakRef = new WeakRef(globalThis.objectToDestroy); + }); + await page.evaluate(() => globalThis.objectToDestroy = null); + await page.forceGarbageCollection(); + expect(await page.evaluate(() => globalThis.weakRef.deref())).toBe(undefined); +});