diff --git a/packages/playwright-test/src/isomorphic/stringInternPool.ts b/packages/playwright-test/src/isomorphic/stringInternPool.ts new file mode 100644 index 0000000000..0d5f3f47c9 --- /dev/null +++ b/packages/playwright-test/src/isomorphic/stringInternPool.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +export class StringInternPool { + private _stringCache = new Map(); + + public internString(s: string): string { + let result = this._stringCache.get(s); + if (!result) { + this._stringCache.set(s, s); + result = s; + } + return result; + } +} \ No newline at end of file diff --git a/packages/playwright-test/src/isomorphic/teleReceiver.ts b/packages/playwright-test/src/isomorphic/teleReceiver.ts index 44d02b0423..8fb93032b7 100644 --- a/packages/playwright-test/src/isomorphic/teleReceiver.ts +++ b/packages/playwright-test/src/isomorphic/teleReceiver.ts @@ -20,6 +20,7 @@ import type { FullProject, Metadata } from '../../types/test'; import type * as reporterTypes from '../../types/testReporter'; import type { SuitePrivate } from '../../types/reporterPrivate'; import type { ReporterV2 } from '../reporters/reporterV2'; +import { StringInternPool } from './stringInternPool'; export type JsonLocation = Location; export type JsonError = string; @@ -130,6 +131,7 @@ export class TeleReporterReceiver { private _reuseTestCases: boolean; private _reportConfig: MergeReporterConfig | undefined; private _config!: FullConfig; + private _stringPool = new StringInternPool(); constructor(pathSeparator: string, reporter: ReporterV2, reuseTestCases: boolean, reportConfig?: MergeReporterConfig) { this._rootSuite = new TeleSuite('', 'root'); @@ -248,6 +250,8 @@ export class TeleReporterReceiver { result.error = result.errors?.[0]; result.attachments = this._parseAttachments(payload.attachments); this._reporter.onTestEnd?.(test, result); + // Free up the memory as won't see these step ids. + result.stepMap = new Map(); } private _onStepBegin(testId: string, resultId: string, payload: JsonTestStepStart) { @@ -255,19 +259,8 @@ export class TeleReporterReceiver { const result = test.resultsMap.get(resultId)!; const parentStep = payload.parentStepId ? result.stepMap.get(payload.parentStepId) : undefined; - const step: TestStep = { - titlePath: () => { - const parentPath = parentStep?.titlePath() || []; - return [...parentPath, payload.title]; - }, - title: payload.title, - category: payload.category, - location: this._absoluteLocation(payload.location), - parent: parentStep, - startTime: new Date(payload.startTime), - duration: -1, - steps: [], - }; + const location = this._absoluteLocation(payload.location); + const step = new TeleTestStep(payload, parentStep, location); if (parentStep) parentStep.steps.push(step); else @@ -307,6 +300,8 @@ export class TeleReporterReceiver { } private _onExit(): Promise | void { + // Free up the memory from the string pool. + this._stringPool = new StringInternPool(); return this._reporter.onExit?.(); } @@ -403,7 +398,7 @@ export class TeleReporterReceiver { private _absolutePath(relativePath?: string): string | undefined { if (!relativePath) return relativePath; - return this._rootDir + this._pathSeparator + relativePath; + return this._stringPool.internString(this._rootDir + this._pathSeparator + relativePath); } } @@ -503,31 +498,57 @@ export class TeleTestCase implements reporterTypes.TestCase { } _createTestResult(id: string): TeleTestResult { - const result: TeleTestResult = { - retry: this.results.length, - parallelIndex: -1, - workerIndex: -1, - duration: -1, - startTime: new Date(), - stdout: [], - stderr: [], - attachments: [], - status: 'skipped', - statusEx: 'scheduled', - steps: [], - errors: [], - stepMap: new Map(), - }; + const result = new TeleTestResult(this.results.length); this.results.push(result); this.resultsMap.set(id, result); return result; } } -export type TeleTestResult = reporterTypes.TestResult & { - stepMap: Map; - statusEx: reporterTypes.TestResult['status'] | 'scheduled' | 'running'; -}; +class TeleTestStep implements TestStep { + title: string; + category: string; + location: Location | undefined; + parent: TestStep | undefined; + startTime: Date; + duration: number = -1; + steps: TestStep[] = []; + + constructor(payload: JsonTestStepStart, parentStep: TestStep | undefined, location: Location | undefined) { + this.title = payload.title; + this.category = payload.category; + this.location = location; + this.parent = parentStep; + this.startTime = new Date(payload.startTime); + } + + titlePath() { + const parentPath = this.parent?.titlePath() || []; + return [...parentPath, this.title]; + } +} + +class TeleTestResult implements reporterTypes.TestResult { + retry: reporterTypes.TestResult['retry']; + parallelIndex: reporterTypes.TestResult['parallelIndex'] = -1; + workerIndex: reporterTypes.TestResult['workerIndex'] = -1; + duration: reporterTypes.TestResult['duration'] = -1; + startTime: reporterTypes.TestResult['startTime'] = new Date(); + stdout: reporterTypes.TestResult['stdout'] = []; + stderr: reporterTypes.TestResult['stderr'] = []; + attachments: reporterTypes.TestResult['attachments'] = []; + status: TestStatus = 'skipped'; + steps: TeleTestStep[] = []; + errors: reporterTypes.TestResult['errors'] = []; + error: reporterTypes.TestResult['error']; + + stepMap: Map = new Map(); + statusEx: reporterTypes.TestResult['status'] | 'scheduled' | 'running' = 'scheduled'; + + constructor(retry: number) { + this.retry = retry; + } +} export type TeleFullProject = FullProject & { id: string }; diff --git a/packages/playwright-test/src/reporters/merge.ts b/packages/playwright-test/src/reporters/merge.ts index d3e34ce986..c4f715badf 100644 --- a/packages/playwright-test/src/reporters/merge.ts +++ b/packages/playwright-test/src/reporters/merge.ts @@ -21,6 +21,7 @@ import type { FullResult } from '../../types/testReporter'; import type { FullConfigInternal } from '../common/config'; import type { JsonConfig, JsonEvent, JsonProject, JsonSuite, JsonTestResultEnd } from '../isomorphic/teleReceiver'; import { TeleReporterReceiver } from '../isomorphic/teleReceiver'; +import { StringInternPool } from '../isomorphic/stringInternPool'; import { createReporters } from '../runner/reporters'; import { Multiplexer } from './multiplexer'; import { ZipFile } from 'playwright-core/lib/utils'; @@ -241,6 +242,7 @@ function printStatusToStdout(message: string) { class ProjectNamePatcher { private _testIds = new Set(); + private _stringPool = new StringInternPool(); constructor(private _allTestIds: Set, private _projectNameSuffix: string) { } @@ -305,6 +307,6 @@ class ProjectNamePatcher { // make them unique and produce a separate test from each blob. while (this._allTestIds.has(testId)) testId = testId + '1'; - return testId; + return this._stringPool.internString(testId); } }