chore: dispose stale handles to prevent oom, 1000 of a kind max (#27315)
https://github.com/microsoft/playwright/issues/6319
This commit is contained in:
		
							parent
							
								
									6181960898
								
							
						
					
					
						commit
						ffd20f43f8
					
				| 
						 | 
				
			
			@ -47,3 +47,5 @@ jobs:
 | 
			
		|||
      if: always()
 | 
			
		||||
    - run: npm run stest browsers -- --project=firefox
 | 
			
		||||
      if: always()
 | 
			
		||||
    - run: npm run stest heap -- --project=chromium
 | 
			
		||||
      if: always()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
 | 
			
		|||
  _logger: Logger | undefined;
 | 
			
		||||
  readonly _instrumentation: ClientInstrumentation;
 | 
			
		||||
  private _eventToSubscriptionMapping: Map<string, string> = new Map();
 | 
			
		||||
  _wasCollected: boolean = false;
 | 
			
		||||
 | 
			
		||||
  constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits<T>) {
 | 
			
		||||
    super();
 | 
			
		||||
| 
						 | 
				
			
			@ -114,15 +115,16 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
 | 
			
		|||
    child._parent = this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _dispose() {
 | 
			
		||||
  _dispose(reason: 'gc' | undefined) {
 | 
			
		||||
    // Clean up from parent and connection.
 | 
			
		||||
    if (this._parent)
 | 
			
		||||
      this._parent._objects.delete(this._guid);
 | 
			
		||||
    this._connection._objects.delete(this._guid);
 | 
			
		||||
    this._wasCollected = reason === 'gc';
 | 
			
		||||
 | 
			
		||||
    // Dispose all children.
 | 
			
		||||
    for (const object of [...this._objects.values()])
 | 
			
		||||
      object._dispose();
 | 
			
		||||
      object._dispose(reason);
 | 
			
		||||
    this._objects.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,6 +116,8 @@ export class Connection extends EventEmitter {
 | 
			
		|||
  async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null, wallTime: number | undefined): Promise<any> {
 | 
			
		||||
    if (this._closedErrorMessage)
 | 
			
		||||
      throw new Error(this._closedErrorMessage);
 | 
			
		||||
    if (object._wasCollected)
 | 
			
		||||
      throw new Error('The object has been collected to prevent unbounded heap growth.');
 | 
			
		||||
 | 
			
		||||
    const { apiName, frames } = stackTrace || { apiName: '', frames: [] };
 | 
			
		||||
    const guid = object._guid;
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +172,7 @@ export class Connection extends EventEmitter {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (method === '__dispose__') {
 | 
			
		||||
      object._dispose();
 | 
			
		||||
      object._dispose(params.reason);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,11 @@ export function existingDispatcher<DispatcherType>(object: any): DispatcherType
 | 
			
		|||
  return object[dispatcherSymbol];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let maxDispatchers = 1000;
 | 
			
		||||
export function setMaxDispatchersForTest(value: number | undefined) {
 | 
			
		||||
  maxDispatchers = value || 1000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeType extends DispatcherScope> extends EventEmitter implements channels.Channel {
 | 
			
		||||
  private _connection: DispatcherConnection;
 | 
			
		||||
  // Parent is always "isScope".
 | 
			
		||||
| 
						 | 
				
			
			@ -55,18 +60,18 @@ export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeT
 | 
			
		|||
    this._parent = parent instanceof DispatcherConnection ? undefined : parent;
 | 
			
		||||
 | 
			
		||||
    const guid = object.guid;
 | 
			
		||||
    assert(!this._connection._dispatchers.has(guid));
 | 
			
		||||
    this._connection._dispatchers.set(guid, this);
 | 
			
		||||
    this._guid = guid;
 | 
			
		||||
    this._type = type;
 | 
			
		||||
    this._object = object;
 | 
			
		||||
 | 
			
		||||
    (object as any)[dispatcherSymbol] = this;
 | 
			
		||||
 | 
			
		||||
    this._connection.registerDispatcher(this);
 | 
			
		||||
    if (this._parent) {
 | 
			
		||||
      assert(!this._parent._dispatchers.has(guid));
 | 
			
		||||
      this._parent._dispatchers.set(guid, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._type = type;
 | 
			
		||||
    this._guid = guid;
 | 
			
		||||
    this._object = object;
 | 
			
		||||
 | 
			
		||||
    (object as any)[dispatcherSymbol] = this;
 | 
			
		||||
    if (this._parent)
 | 
			
		||||
      this._connection.sendCreate(this._parent, type, guid, initializer, this._parent._object);
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -100,9 +105,9 @@ export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeT
 | 
			
		|||
    this._connection.sendEvent(this, method as string, params, sdkObject);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _dispose() {
 | 
			
		||||
  _dispose(reason?: 'gc') {
 | 
			
		||||
    this._disposeRecursively();
 | 
			
		||||
    this._connection.sendDispose(this);
 | 
			
		||||
    this._connection.sendDispose(this, reason);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected _onDispose() {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,8 +120,9 @@ export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeT
 | 
			
		|||
    eventsHelper.removeEventListeners(this._eventListeners);
 | 
			
		||||
 | 
			
		||||
    // Clean up from parent and connection.
 | 
			
		||||
    if (this._parent)
 | 
			
		||||
      this._parent._dispatchers.delete(this._guid);
 | 
			
		||||
    this._parent?._dispatchers.delete(this._guid);
 | 
			
		||||
    const list = this._connection._dispatchersByType.get(this._type);
 | 
			
		||||
    list?.delete(this._guid);
 | 
			
		||||
    this._connection._dispatchers.delete(this._guid);
 | 
			
		||||
 | 
			
		||||
    // Dispose all children.
 | 
			
		||||
| 
						 | 
				
			
			@ -159,6 +165,8 @@ export class RootDispatcher extends Dispatcher<{ guid: '' }, any, any> {
 | 
			
		|||
 | 
			
		||||
export class DispatcherConnection {
 | 
			
		||||
  readonly _dispatchers = new Map<string, DispatcherScope>();
 | 
			
		||||
  // Collect stale dispatchers by type.
 | 
			
		||||
  readonly _dispatchersByType = new Map<string, Set<string>>();
 | 
			
		||||
  onmessage = (message: object) => {};
 | 
			
		||||
  private _waitOperations = new Map<string, CallMetadata>();
 | 
			
		||||
  private _isLocal: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -183,8 +191,8 @@ export class DispatcherConnection {
 | 
			
		|||
    this._sendMessageToClient(parent._guid, dispatcher._type, '__adopt__', { guid: dispatcher._guid });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sendDispose(dispatcher: DispatcherScope) {
 | 
			
		||||
    this._sendMessageToClient(dispatcher._guid, dispatcher._type, '__dispose__', {});
 | 
			
		||||
  sendDispose(dispatcher: DispatcherScope, reason?: 'gc') {
 | 
			
		||||
    this._sendMessageToClient(dispatcher._guid, dispatcher._type, '__dispose__', { reason });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
 | 
			
		||||
| 
						 | 
				
			
			@ -224,6 +232,32 @@ export class DispatcherConnection {
 | 
			
		|||
    throw new ValidationError(`${path}: expected dispatcher ${names.toString()}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  registerDispatcher(dispatcher: DispatcherScope) {
 | 
			
		||||
    assert(!this._dispatchers.has(dispatcher._guid));
 | 
			
		||||
    this._dispatchers.set(dispatcher._guid, dispatcher);
 | 
			
		||||
    const type = dispatcher._type;
 | 
			
		||||
 | 
			
		||||
    let list = this._dispatchersByType.get(type);
 | 
			
		||||
    if (!list) {
 | 
			
		||||
      list = new Set();
 | 
			
		||||
      this._dispatchersByType.set(type, list);
 | 
			
		||||
    }
 | 
			
		||||
    list.add(dispatcher._guid);
 | 
			
		||||
    if (list.size > maxDispatchers)
 | 
			
		||||
      this._disposeStaleDispatchers(type, list);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _disposeStaleDispatchers(type: string, dispatchers: Set<string>) {
 | 
			
		||||
    const dispatchersArray = [...dispatchers];
 | 
			
		||||
    this._dispatchersByType.set(type, new Set(dispatchersArray.slice(maxDispatchers / 10)));
 | 
			
		||||
    for (let i = 0; i < maxDispatchers / 10; ++i) {
 | 
			
		||||
      const d = this._dispatchers.get(dispatchersArray[i]);
 | 
			
		||||
      if (!d)
 | 
			
		||||
        continue;
 | 
			
		||||
      d._dispose('gc');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async dispatch(message: object) {
 | 
			
		||||
    const { id, guid, method, params, metadata } = message as any;
 | 
			
		||||
    const dispatcher = this._dispatchers.get(guid);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ import { contextTest as test, expect } from '../config/browserTest';
 | 
			
		|||
import { queryObjectCount } from '../config/queryObjects';
 | 
			
		||||
 | 
			
		||||
test.describe.configure({ mode: 'serial' });
 | 
			
		||||
test.skip(({ browserName }) => browserName !== 'chromium');
 | 
			
		||||
 | 
			
		||||
for (let i = 0; i < 3; ++i) {
 | 
			
		||||
  test(`test #${i} to request page and context`, async ({ page, context }) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -65,7 +66,7 @@ test('should not leak dispatchers after closing page', async ({ context, server
 | 
			
		|||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(COUNT);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBe(COUNT);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(COUNT);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/consoleMessageDispatcher').ConsoleMessageDispatcher)).toBe(COUNT);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/console').ConsoleMessage)).toBe(0);
 | 
			
		||||
 | 
			
		||||
  for (const page of pages)
 | 
			
		||||
    await page.close();
 | 
			
		||||
| 
						 | 
				
			
			@ -74,10 +75,46 @@ test('should not leak dispatchers after closing page', async ({ context, server
 | 
			
		|||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBe(0);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(0);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/consoleMessageDispatcher').ConsoleMessageDispatcher)).toBe(0);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/console').ConsoleMessage)).toBe(0);
 | 
			
		||||
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/page').Page)).toBeLessThan(COUNT);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Request)).toBe(0);
 | 
			
		||||
  expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Response)).toBe(0);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe(() => {
 | 
			
		||||
  test.beforeEach(() => {
 | 
			
		||||
    require('../../packages/playwright-core/lib/server/dispatchers/dispatcher').setMaxDispatchersForTest(100);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('should collect stale handles', async ({ page, server }) => {
 | 
			
		||||
    page.on('request', () => {});
 | 
			
		||||
    const response = await page.goto(server.PREFIX + '/title.html');
 | 
			
		||||
    for (let i = 0; i < 200; ++i) {
 | 
			
		||||
      await page.evaluate(async () => {
 | 
			
		||||
        const response = await fetch('/');
 | 
			
		||||
        await response.text();
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    const e = await response.allHeaders().catch(e => e);
 | 
			
		||||
    expect(e.message).toContain('The object has been collected to prevent unbounded heap growth.');
 | 
			
		||||
 | 
			
		||||
    const counts = [
 | 
			
		||||
      { count: await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Request), message: 'client.Request' },
 | 
			
		||||
      { count: await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Response), message: 'client.Response' },
 | 
			
		||||
      { count: await queryObjectCount(require('../../packages/playwright-core/lib/server/network').Request), message: 'server.Request' },
 | 
			
		||||
      { count: await queryObjectCount(require('../../packages/playwright-core/lib/server/network').Response), message: 'server.Response' },
 | 
			
		||||
      { count: await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher), message: 'dispatchers.RequestDispatcher' },
 | 
			
		||||
      { count: await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher), message: 'dispatchers.ResponseDispatcher' },
 | 
			
		||||
    ];
 | 
			
		||||
    for (const { count, message } of counts) {
 | 
			
		||||
      expect(count, { message }).toBeGreaterThan(50);
 | 
			
		||||
      expect(count, { message }).toBeLessThan(150);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.afterEach(() => {
 | 
			
		||||
    require('../../packages/playwright-core/lib/server/dispatchers/dispatcher').setMaxDispatchersForTest(null);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue