@elliemae/encw-leak-runner 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +63 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin/leak-runner.js +258 -29
- package/dist/cjs/browser/heapMemoryProfiler.js +182 -0
- package/dist/cjs/browser/iframeHeapProfiler.js +2 -2
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/runner/scenarioRunner.js +9 -6
- package/dist/cjs/scenarios/index.js +7 -4
- package/dist/cjs/scenarios/one-admin/index.js +5 -1
- package/dist/cjs/scenarios/pipeline/index.js +31 -0
- package/dist/cjs/scenarios/pipeline/page-models/PipelinePageModel.js +52 -0
- package/dist/cjs/scenarios/pipeline/page-models/index.js +24 -0
- package/dist/cjs/scenarios/pipeline/pipeline-task-navigation.scenario.js +56 -0
- package/dist/esm/browser/heapMemoryProfiler.js +152 -0
- package/dist/esm/browser/iframeHeapProfiler.js +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/runner/scenarioRunner.js +9 -6
- package/dist/esm/scenarios/index.js +8 -5
- package/dist/esm/scenarios/one-admin/index.js +5 -1
- package/dist/esm/scenarios/pipeline/index.js +11 -0
- package/dist/esm/scenarios/pipeline/page-models/PipelinePageModel.js +32 -0
- package/dist/esm/scenarios/pipeline/page-models/index.js +4 -0
- package/dist/esm/scenarios/pipeline/pipeline-task-navigation.scenario.js +36 -0
- package/dist/types/lib/browser/heapMemoryProfiler.d.ts +149 -0
- package/dist/types/lib/browser/iframeHeapProfiler.d.ts +1 -1
- package/dist/types/lib/browser/tests/heapMemoryProfiler.test.d.ts +1 -0
- package/dist/types/lib/config/runnerConfigSchema.d.ts +6 -6
- package/dist/types/lib/index.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/index.d.ts +2 -2
- package/dist/types/lib/scenarios/pipeline/index.d.ts +2 -0
- package/dist/types/lib/scenarios/pipeline/page-models/PipelinePageModel.d.ts +14 -0
- package/dist/types/lib/scenarios/pipeline/page-models/index.d.ts +1 -0
- package/dist/types/lib/scenarios/pipeline/pipeline-task-navigation.scenario.d.ts +2 -0
- package/leak-runner.config.json +1 -1
- package/lib/browser/heapMemoryProfiler.ts +284 -0
- package/lib/browser/iframeHeapProfiler.ts +1 -1
- package/lib/browser/tests/heapMemoryProfiler.test.ts +64 -0
- package/lib/browser/tests/iframeHeapProfiler.test.ts +1 -1
- package/lib/index.ts +2 -0
- package/lib/runner/scenarioRunner.ts +9 -6
- package/lib/scenarios/index.ts +8 -6
- package/lib/scenarios/one-admin/index.ts +7 -1
- package/lib/scenarios/pipeline/index.ts +12 -0
- package/lib/scenarios/pipeline/page-models/PipelinePageModel.ts +46 -0
- package/lib/scenarios/pipeline/page-models/index.ts +1 -0
- package/lib/scenarios/pipeline/pipeline-task-navigation.scenario.ts +42 -0
- package/package.json +5 -4
- package/reports/analysis/index.html +1 -1
- package/reports/analysis/thresholdEvaluator.ts.html +1 -1
- package/reports/browser/heapMemoryProfiler.ts.html +937 -0
- package/reports/browser/iframeHeapProfiler.ts.html +5 -5
- package/reports/browser/index.html +25 -10
- package/reports/cli/commands/index.html +1 -1
- package/reports/cli/commands/listCommand.ts.html +1 -1
- package/reports/cli/commands/runCommand.ts.html +1 -1
- package/reports/cli/index.html +1 -1
- package/reports/cli/index.ts.html +1 -1
- package/reports/config/index.html +1 -1
- package/reports/config/missingRequiredParamError.ts.html +1 -1
- package/reports/config/requiredEnvParams.ts.html +1 -1
- package/reports/config/runnerConfigLoader.ts.html +1 -1
- package/reports/config/runnerConfigSchema.ts.html +1 -1
- package/reports/config/sources/cliOverrideConfigSource.ts.html +1 -1
- package/reports/config/sources/configSource.ts.html +1 -1
- package/reports/config/sources/envVarConfigSource.ts.html +1 -1
- package/reports/config/sources/fileConfigSource.ts.html +1 -1
- package/reports/config/sources/index.html +1 -1
- package/reports/config/unknownEnvError.ts.html +1 -1
- package/reports/index.html +63 -33
- package/reports/lcov-report/analysis/index.html +1 -1
- package/reports/lcov-report/analysis/thresholdEvaluator.ts.html +1 -1
- package/reports/lcov-report/browser/heapMemoryProfiler.ts.html +937 -0
- package/reports/lcov-report/browser/iframeHeapProfiler.ts.html +5 -5
- package/reports/lcov-report/browser/index.html +25 -10
- package/reports/lcov-report/cli/commands/index.html +1 -1
- package/reports/lcov-report/cli/commands/listCommand.ts.html +1 -1
- package/reports/lcov-report/cli/commands/runCommand.ts.html +1 -1
- package/reports/lcov-report/cli/index.html +1 -1
- package/reports/lcov-report/cli/index.ts.html +1 -1
- package/reports/lcov-report/config/index.html +1 -1
- package/reports/lcov-report/config/missingRequiredParamError.ts.html +1 -1
- package/reports/lcov-report/config/requiredEnvParams.ts.html +1 -1
- package/reports/lcov-report/config/runnerConfigLoader.ts.html +1 -1
- package/reports/lcov-report/config/runnerConfigSchema.ts.html +1 -1
- package/reports/lcov-report/config/sources/cliOverrideConfigSource.ts.html +1 -1
- package/reports/lcov-report/config/sources/configSource.ts.html +1 -1
- package/reports/lcov-report/config/sources/envVarConfigSource.ts.html +1 -1
- package/reports/lcov-report/config/sources/fileConfigSource.ts.html +1 -1
- package/reports/lcov-report/config/sources/index.html +1 -1
- package/reports/lcov-report/config/unknownEnvError.ts.html +1 -1
- package/reports/lcov-report/index.html +63 -33
- package/reports/lcov-report/registry/index.html +1 -1
- package/reports/lcov-report/registry/scenarioRegistry.ts.html +1 -1
- package/reports/lcov-report/reporting/consoleReporter.ts.html +1 -1
- package/reports/lcov-report/reporting/index.html +1 -1
- package/reports/lcov-report/reporting/junitReporter.ts.html +1 -1
- package/reports/lcov-report/runner/aiEnhancementStep.ts.html +1 -1
- package/reports/lcov-report/runner/batchRunner.ts.html +1 -1
- package/reports/lcov-report/runner/index.html +15 -15
- package/reports/lcov-report/runner/scenarioRunner.ts.html +23 -14
- package/reports/lcov-report/scenarios/index.html +8 -8
- package/reports/lcov-report/scenarios/index.ts.html +20 -14
- package/reports/lcov-report/scenarios/one-admin/export-navigation.scenario.ts.html +1 -1
- package/reports/lcov-report/scenarios/one-admin/index.html +5 -5
- package/reports/lcov-report/scenarios/one-admin/index.ts.html +24 -6
- package/reports/lcov-report/scenarios/one-admin/page-models/AdminLandingPageModel.ts.html +1 -1
- package/reports/lcov-report/scenarios/one-admin/page-models/ExportPageModel.ts.html +1 -1
- package/reports/lcov-report/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +1 -1
- package/reports/lcov-report/scenarios/one-admin/page-models/index.html +1 -1
- package/reports/lcov-report/scenarios/pipeline/index.html +131 -0
- package/reports/lcov-report/scenarios/pipeline/index.ts.html +121 -0
- package/reports/lcov-report/scenarios/pipeline/page-models/PipelinePageModel.ts.html +223 -0
- package/reports/lcov-report/scenarios/pipeline/page-models/index.html +116 -0
- package/reports/lcov-report/scenarios/pipeline/pipeline-task-navigation.scenario.ts.html +211 -0
- package/reports/lcov-report/types/config.ts.html +1 -1
- package/reports/lcov-report/types/index.html +1 -1
- package/reports/lcov.info +225 -40
- package/reports/registry/index.html +1 -1
- package/reports/registry/scenarioRegistry.ts.html +1 -1
- package/reports/reporting/consoleReporter.ts.html +1 -1
- package/reports/reporting/index.html +1 -1
- package/reports/reporting/junitReporter.ts.html +1 -1
- package/reports/runner/aiEnhancementStep.ts.html +1 -1
- package/reports/runner/batchRunner.ts.html +1 -1
- package/reports/runner/index.html +15 -15
- package/reports/runner/scenarioRunner.ts.html +23 -14
- package/reports/scenarios/index.html +8 -8
- package/reports/scenarios/index.ts.html +20 -14
- package/reports/scenarios/one-admin/export-navigation.scenario.ts.html +1 -1
- package/reports/scenarios/one-admin/index.html +5 -5
- package/reports/scenarios/one-admin/index.ts.html +24 -6
- package/reports/scenarios/one-admin/page-models/AdminLandingPageModel.ts.html +1 -1
- package/reports/scenarios/one-admin/page-models/ExportPageModel.ts.html +1 -1
- package/reports/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +1 -1
- package/reports/scenarios/one-admin/page-models/index.html +1 -1
- package/reports/scenarios/pipeline/index.html +131 -0
- package/reports/scenarios/pipeline/index.ts.html +121 -0
- package/reports/scenarios/pipeline/page-models/PipelinePageModel.ts.html +223 -0
- package/reports/scenarios/pipeline/page-models/index.html +116 -0
- package/reports/scenarios/pipeline/pipeline-task-navigation.scenario.ts.html +211 -0
- package/reports/types/config.ts.html +1 -1
- package/reports/types/index.html +1 -1
- package/test-report.xml +57 -53
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pipeline_exports = {};
|
|
20
|
+
__export(pipeline_exports, {
|
|
21
|
+
pipelineScenarioGroup: () => pipelineScenarioGroup
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(pipeline_exports);
|
|
24
|
+
var import_pipeline_task_navigation_scenario = require("./pipeline-task-navigation.scenario.js");
|
|
25
|
+
const pipelineScenarios = [
|
|
26
|
+
import_pipeline_task_navigation_scenario.pipelineTaskNavigationScenario
|
|
27
|
+
];
|
|
28
|
+
const pipelineScenarioGroup = {
|
|
29
|
+
microapp: "pipeline",
|
|
30
|
+
scenarios: pipelineScenarios
|
|
31
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var PipelinePageModel_exports = {};
|
|
20
|
+
__export(PipelinePageModel_exports, {
|
|
21
|
+
PipelinePageModel: () => PipelinePageModel
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(PipelinePageModel_exports);
|
|
24
|
+
class PipelinePageModel {
|
|
25
|
+
constructor(page) {
|
|
26
|
+
this.page = page;
|
|
27
|
+
}
|
|
28
|
+
page;
|
|
29
|
+
static IFRAME_SELECTOR = '[data-testid="pui-iframe-container-pipelineui"]';
|
|
30
|
+
static TASKS_IFRAME_SELECTOR = '[data-testid="pui-iframe-container-taskspipeline"]';
|
|
31
|
+
static SPINNER_TEST_ID = "ds-circularindeterminateindicator-svg";
|
|
32
|
+
async waitForPipelineReady() {
|
|
33
|
+
await this.#waitForSpinner(PipelinePageModel.IFRAME_SELECTOR);
|
|
34
|
+
}
|
|
35
|
+
async clickTasksTab() {
|
|
36
|
+
await this.page.getByRole("tab", { name: "Tasks" }).click();
|
|
37
|
+
}
|
|
38
|
+
async clickPipelineTab() {
|
|
39
|
+
await this.page.getByRole("tab", { name: "Loans" }).click();
|
|
40
|
+
}
|
|
41
|
+
async waitForPipelineSpinner() {
|
|
42
|
+
await this.#waitForSpinner(PipelinePageModel.IFRAME_SELECTOR);
|
|
43
|
+
}
|
|
44
|
+
async waitForTasksReady() {
|
|
45
|
+
await this.page.locator(PipelinePageModel.TASKS_IFRAME_SELECTOR).contentFrame().getByTestId("circular-indicator").waitFor({ state: "hidden" });
|
|
46
|
+
}
|
|
47
|
+
async #waitForSpinner(iframeSelector) {
|
|
48
|
+
const spinner = this.page.frameLocator(iframeSelector).getByTestId(PipelinePageModel.SPINNER_TEST_ID);
|
|
49
|
+
await spinner.waitFor({ state: "visible" });
|
|
50
|
+
await spinner.waitFor({ state: "hidden" });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var page_models_exports = {};
|
|
20
|
+
__export(page_models_exports, {
|
|
21
|
+
PipelinePageModel: () => import_PipelinePageModel.PipelinePageModel
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(page_models_exports);
|
|
24
|
+
var import_PipelinePageModel = require("./PipelinePageModel.js");
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pipeline_task_navigation_scenario_exports = {};
|
|
20
|
+
__export(pipeline_task_navigation_scenario_exports, {
|
|
21
|
+
pipelineTaskNavigationScenario: () => pipelineTaskNavigationScenario
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(pipeline_task_navigation_scenario_exports);
|
|
24
|
+
var import_page_models = require("../one-admin/page-models/index.js");
|
|
25
|
+
var import_page_models2 = require("./page-models/index.js");
|
|
26
|
+
const pipelineTaskNavigationScenario = {
|
|
27
|
+
id: "pipeline-task-navigation",
|
|
28
|
+
name: "Pipeline Task Navigation",
|
|
29
|
+
description: "Navigate to pipeline task details and come back - verify iframe GC",
|
|
30
|
+
tags: ["critical"],
|
|
31
|
+
microappSelector: import_page_models2.PipelinePageModel.IFRAME_SELECTOR,
|
|
32
|
+
url: () => "/pipeline",
|
|
33
|
+
async setup(page) {
|
|
34
|
+
await import_page_models.AdminLandingPageModel.acceptCookiesIfShown(page);
|
|
35
|
+
const pipeline = new import_page_models2.PipelinePageModel(page);
|
|
36
|
+
await pipeline.waitForPipelineReady();
|
|
37
|
+
},
|
|
38
|
+
async action(page) {
|
|
39
|
+
const pipeline = new import_page_models2.PipelinePageModel(page);
|
|
40
|
+
const path = new URL(page.url()).pathname.replace(/\/$/, "");
|
|
41
|
+
if (path === "/pipeline") {
|
|
42
|
+
await pipeline.clickTasksTab();
|
|
43
|
+
}
|
|
44
|
+
await pipeline.waitForTasksReady();
|
|
45
|
+
},
|
|
46
|
+
async back(page) {
|
|
47
|
+
const pipeline = new import_page_models2.PipelinePageModel(page);
|
|
48
|
+
await pipeline.clickPipelineTab();
|
|
49
|
+
await pipeline.waitForPipelineSpinner();
|
|
50
|
+
},
|
|
51
|
+
repeat: () => 1,
|
|
52
|
+
thresholds: {
|
|
53
|
+
maxRetainedSizeDeltaBytes: 10 * 1024 * 1024,
|
|
54
|
+
maxNewLeakGroups: 0
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { HeapDoctor } from "@elliemae/encw-heap-doctor";
|
|
4
|
+
class HeapMemoryProfiler {
|
|
5
|
+
/**
|
|
6
|
+
* @param {Page} page - The Playwright `Page` instance for the current test.
|
|
7
|
+
* @param {string} outputDir - Directory where `.heapsnapshot` and `.md` files are written.
|
|
8
|
+
* Created automatically if it does not exist.
|
|
9
|
+
*/
|
|
10
|
+
constructor(page, outputDir) {
|
|
11
|
+
this.page = page;
|
|
12
|
+
this.outputDir = outputDir;
|
|
13
|
+
}
|
|
14
|
+
page;
|
|
15
|
+
outputDir;
|
|
16
|
+
/** Memento store: maps snapshot label → absolute file path on disk. */
|
|
17
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
18
|
+
getCDPTarget() {
|
|
19
|
+
return this.page;
|
|
20
|
+
}
|
|
21
|
+
// ─── Browser guard ──────────────────────────────────────────────────────────
|
|
22
|
+
isChromium() {
|
|
23
|
+
return this.page.context().browser()?.browserType().name() === "chromium";
|
|
24
|
+
}
|
|
25
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Capture a Chrome heap snapshot via CDP and persist it to disk.
|
|
28
|
+
*
|
|
29
|
+
* The snapshot is streamed in chunks via `HeapProfiler.addHeapSnapshotChunk`,
|
|
30
|
+
* joined, and written as a single `.heapsnapshot` file. The file path is cached
|
|
31
|
+
* internally under `label` so {@link compare} can resolve it by name.
|
|
32
|
+
* @param {string} label - Logical name for this snapshot (e.g. `'before'`, `'after'`).
|
|
33
|
+
* Used as the filename prefix and as the Memento key.
|
|
34
|
+
* @returns {Promise<string>} Absolute path to the written file, or `''` on non-Chromium browsers.
|
|
35
|
+
*/
|
|
36
|
+
async captureSnapshot(label) {
|
|
37
|
+
if (!this.isChromium()) return "";
|
|
38
|
+
const client = await this.page.context().newCDPSession(this.getCDPTarget());
|
|
39
|
+
await client.send("HeapProfiler.enable");
|
|
40
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
41
|
+
const filePath = path.join(
|
|
42
|
+
this.outputDir,
|
|
43
|
+
`${label}-${Date.now()}.heapsnapshot`
|
|
44
|
+
);
|
|
45
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
46
|
+
client.on(
|
|
47
|
+
"HeapProfiler.addHeapSnapshotChunk",
|
|
48
|
+
({ chunk }) => {
|
|
49
|
+
writeStream.write(chunk);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
await client.send("HeapProfiler.takeHeapSnapshot", {
|
|
53
|
+
reportProgress: false
|
|
54
|
+
});
|
|
55
|
+
await client.send("HeapProfiler.disable");
|
|
56
|
+
await client.detach();
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
writeStream.end((err) => {
|
|
59
|
+
if (err) reject(err);
|
|
60
|
+
else resolve();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
this.snapshots.set(label, filePath);
|
|
64
|
+
return filePath;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Compare two previously captured snapshots by their labels.
|
|
68
|
+
*
|
|
69
|
+
* Runs `HeapDoctor.compare()` on the resolved file paths and writes a
|
|
70
|
+
* markdown report to {@link outputDir}.
|
|
71
|
+
* @param {CompareParams} params - {@link CompareParams}
|
|
72
|
+
* @returns {Promise<ComparisonReport | null>} `ComparisonReport`, or `null` on non-Chromium browsers.
|
|
73
|
+
* @throws If either label has no cached snapshot path.
|
|
74
|
+
* @throws If `HeapDoctor.compare` returns `ok: false`.
|
|
75
|
+
*/
|
|
76
|
+
async compare(params) {
|
|
77
|
+
const { beforeLabel, afterLabel, topN, originAllowList } = params;
|
|
78
|
+
if (!this.isChromium()) return null;
|
|
79
|
+
const before = this.snapshots.get(beforeLabel);
|
|
80
|
+
const after = this.snapshots.get(afterLabel);
|
|
81
|
+
if (!before || !after) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`HeapMemoryProfiler: snapshot not found for labels "${beforeLabel}" / "${afterLabel}". Call captureSnapshot() before compare().`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const result = await new HeapDoctor({ topN, originAllowList }).compare(
|
|
87
|
+
before,
|
|
88
|
+
after
|
|
89
|
+
);
|
|
90
|
+
if (!result.ok) throw result.error;
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(this.outputDir, `comparison-${Date.now()}.md`),
|
|
93
|
+
result.value.markdown
|
|
94
|
+
);
|
|
95
|
+
return result.value;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Orchestrate the full heap profiling sequence for a user flow.
|
|
99
|
+
*
|
|
100
|
+
* Sequence (Template Method):
|
|
101
|
+
* 1. Capture **before** snapshot
|
|
102
|
+
* 2. Execute `flow` exactly `heapIterations` times
|
|
103
|
+
* 3. Capture **after** snapshot
|
|
104
|
+
* 4. Run `HeapDoctor.compare` and write the markdown report
|
|
105
|
+
* 5. If `failOnLeak` is `true` and `retainedSizeDelta > leakThresholdBytes`, throw
|
|
106
|
+
*
|
|
107
|
+
* On non-Chromium browsers: `flow` still runs `heapIterations` times,
|
|
108
|
+
* no snapshots are taken, and `null` is returned.
|
|
109
|
+
* @param {() => Promise<void>} flow - Async function containing the user actions to profile.
|
|
110
|
+
* @param {HeapProfilingOptions} [options] - {@link HeapProfilingOptions}
|
|
111
|
+
* @returns {Promise<ComparisonReport | null>} `ComparisonReport` on Chromium, `null` on all other browsers.
|
|
112
|
+
*/
|
|
113
|
+
async withProfiling(flow, options = {}) {
|
|
114
|
+
const {
|
|
115
|
+
heapIterations = 1,
|
|
116
|
+
label = "flow",
|
|
117
|
+
topN,
|
|
118
|
+
failOnLeak = false,
|
|
119
|
+
leakThresholdBytes = 0,
|
|
120
|
+
originAllowList
|
|
121
|
+
} = options;
|
|
122
|
+
if (!this.isChromium()) {
|
|
123
|
+
for (let i = 0; i < heapIterations; i += 1) await flow();
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
await this.captureSnapshot(`${label}-before`);
|
|
127
|
+
for (let i = 0; i < heapIterations; i += 1) await flow();
|
|
128
|
+
await this.captureSnapshot(`${label}-after`);
|
|
129
|
+
const report = await this.compare({
|
|
130
|
+
beforeLabel: `${label}-before`,
|
|
131
|
+
afterLabel: `${label}-after`,
|
|
132
|
+
topN,
|
|
133
|
+
originAllowList
|
|
134
|
+
});
|
|
135
|
+
if (report && failOnLeak && report.delta.retainedSizeDelta > leakThresholdBytes) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Memory leak detected: retained size grew by ${report.delta.retainedSizeDelta} bytes (threshold: ${leakThresholdBytes} bytes). New leak groups: ${report.delta.newLeakGroups.length}. See: ${this.outputDir}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return report;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clear the internal snapshot label cache.
|
|
144
|
+
* Call in `afterEach` to prevent snapshot paths bleeding across tests.
|
|
145
|
+
*/
|
|
146
|
+
clearSnapshots() {
|
|
147
|
+
this.snapshots.clear();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
HeapMemoryProfiler
|
|
152
|
+
};
|
package/dist/esm/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DEFAULT_THRESHOLDS } from "./types/config.js";
|
|
|
2
2
|
import { ScenarioRegistry } from "./registry/scenarioRegistry.js";
|
|
3
3
|
import { BatchRunner } from "./runner/batchRunner.js";
|
|
4
4
|
import { ScenarioRunner } from "./runner/scenarioRunner.js";
|
|
5
|
+
import { HeapMemoryProfiler } from "./browser/heapMemoryProfiler.js";
|
|
5
6
|
import { IframeHeapProfiler } from "./browser/iframeHeapProfiler.js";
|
|
6
7
|
import { ThresholdEvaluator } from "./analysis/thresholdEvaluator.js";
|
|
7
8
|
import { ConsoleReporter } from "./reporting/consoleReporter.js";
|
|
@@ -23,6 +24,7 @@ export {
|
|
|
23
24
|
DEFAULT_THRESHOLDS,
|
|
24
25
|
EnvVarConfigSource,
|
|
25
26
|
FileConfigSource,
|
|
27
|
+
HeapMemoryProfiler,
|
|
26
28
|
IframeHeapProfiler,
|
|
27
29
|
JunitReporter,
|
|
28
30
|
MissingRequiredParamError,
|
|
@@ -68,11 +68,12 @@ class ScenarioRunner {
|
|
|
68
68
|
await this.repeatScenarioActions(scenario, page, frame);
|
|
69
69
|
await forceGarbageCollection(page);
|
|
70
70
|
paths.after = await profiler.captureSnapshot("after");
|
|
71
|
-
const report = await profiler.compare(
|
|
72
|
-
"before",
|
|
73
|
-
"after",
|
|
74
|
-
this.config.runner.topN
|
|
75
|
-
|
|
71
|
+
const report = await profiler.compare({
|
|
72
|
+
beforeLabel: "before",
|
|
73
|
+
afterLabel: "after",
|
|
74
|
+
topN: this.config.runner.topN,
|
|
75
|
+
originAllowList: [new URL(this.config.env.baseUrl).origin]
|
|
76
|
+
});
|
|
76
77
|
if (report) await this.writeReport(report, scenario);
|
|
77
78
|
return report;
|
|
78
79
|
}
|
|
@@ -88,7 +89,9 @@ class ScenarioRunner {
|
|
|
88
89
|
});
|
|
89
90
|
process.stderr.write(`[scenarioRunner] post-login URL = ${page.url()}
|
|
90
91
|
`);
|
|
91
|
-
|
|
92
|
+
if (!page.url().includes(scenario.url())) {
|
|
93
|
+
await page.goto(scenario.url(), { waitUntil: "load", timeout: 3e4 });
|
|
94
|
+
}
|
|
92
95
|
process.stderr.write(
|
|
93
96
|
`[scenarioRunner] settled URL before iframe = ${page.url()}
|
|
94
97
|
`
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ScenarioRegistry } from "../registry/scenarioRegistry.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { oneAdminScenarioGroup } from "./one-admin/index.js";
|
|
3
|
+
import { pipelineScenarioGroup } from "./pipeline/index.js";
|
|
4
|
+
const scenarioRegistry = (() => {
|
|
5
|
+
const registry = new ScenarioRegistry();
|
|
6
|
+
registry.register(oneAdminScenarioGroup);
|
|
7
|
+
registry.register(pipelineScenarioGroup);
|
|
8
|
+
return registry;
|
|
9
|
+
})();
|
|
7
10
|
export {
|
|
8
11
|
scenarioRegistry
|
|
9
12
|
};
|
|
@@ -2,6 +2,10 @@ import { exportNavigationScenario } from "./export-navigation.scenario.js";
|
|
|
2
2
|
const oneAdminScenarios = [
|
|
3
3
|
exportNavigationScenario
|
|
4
4
|
];
|
|
5
|
+
const oneAdminScenarioGroup = {
|
|
6
|
+
microapp: "one-admin",
|
|
7
|
+
scenarios: oneAdminScenarios
|
|
8
|
+
};
|
|
5
9
|
export {
|
|
6
|
-
|
|
10
|
+
oneAdminScenarioGroup
|
|
7
11
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { pipelineTaskNavigationScenario } from "./pipeline-task-navigation.scenario.js";
|
|
2
|
+
const pipelineScenarios = [
|
|
3
|
+
pipelineTaskNavigationScenario
|
|
4
|
+
];
|
|
5
|
+
const pipelineScenarioGroup = {
|
|
6
|
+
microapp: "pipeline",
|
|
7
|
+
scenarios: pipelineScenarios
|
|
8
|
+
};
|
|
9
|
+
export {
|
|
10
|
+
pipelineScenarioGroup
|
|
11
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class PipelinePageModel {
|
|
2
|
+
constructor(page) {
|
|
3
|
+
this.page = page;
|
|
4
|
+
}
|
|
5
|
+
page;
|
|
6
|
+
static IFRAME_SELECTOR = '[data-testid="pui-iframe-container-pipelineui"]';
|
|
7
|
+
static TASKS_IFRAME_SELECTOR = '[data-testid="pui-iframe-container-taskspipeline"]';
|
|
8
|
+
static SPINNER_TEST_ID = "ds-circularindeterminateindicator-svg";
|
|
9
|
+
async waitForPipelineReady() {
|
|
10
|
+
await this.#waitForSpinner(PipelinePageModel.IFRAME_SELECTOR);
|
|
11
|
+
}
|
|
12
|
+
async clickTasksTab() {
|
|
13
|
+
await this.page.getByRole("tab", { name: "Tasks" }).click();
|
|
14
|
+
}
|
|
15
|
+
async clickPipelineTab() {
|
|
16
|
+
await this.page.getByRole("tab", { name: "Loans" }).click();
|
|
17
|
+
}
|
|
18
|
+
async waitForPipelineSpinner() {
|
|
19
|
+
await this.#waitForSpinner(PipelinePageModel.IFRAME_SELECTOR);
|
|
20
|
+
}
|
|
21
|
+
async waitForTasksReady() {
|
|
22
|
+
await this.page.locator(PipelinePageModel.TASKS_IFRAME_SELECTOR).contentFrame().getByTestId("circular-indicator").waitFor({ state: "hidden" });
|
|
23
|
+
}
|
|
24
|
+
async #waitForSpinner(iframeSelector) {
|
|
25
|
+
const spinner = this.page.frameLocator(iframeSelector).getByTestId(PipelinePageModel.SPINNER_TEST_ID);
|
|
26
|
+
await spinner.waitFor({ state: "visible" });
|
|
27
|
+
await spinner.waitFor({ state: "hidden" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
PipelinePageModel
|
|
32
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AdminLandingPageModel } from "../one-admin/page-models/index.js";
|
|
2
|
+
import { PipelinePageModel } from "./page-models/index.js";
|
|
3
|
+
const pipelineTaskNavigationScenario = {
|
|
4
|
+
id: "pipeline-task-navigation",
|
|
5
|
+
name: "Pipeline Task Navigation",
|
|
6
|
+
description: "Navigate to pipeline task details and come back - verify iframe GC",
|
|
7
|
+
tags: ["critical"],
|
|
8
|
+
microappSelector: PipelinePageModel.IFRAME_SELECTOR,
|
|
9
|
+
url: () => "/pipeline",
|
|
10
|
+
async setup(page) {
|
|
11
|
+
await AdminLandingPageModel.acceptCookiesIfShown(page);
|
|
12
|
+
const pipeline = new PipelinePageModel(page);
|
|
13
|
+
await pipeline.waitForPipelineReady();
|
|
14
|
+
},
|
|
15
|
+
async action(page) {
|
|
16
|
+
const pipeline = new PipelinePageModel(page);
|
|
17
|
+
const path = new URL(page.url()).pathname.replace(/\/$/, "");
|
|
18
|
+
if (path === "/pipeline") {
|
|
19
|
+
await pipeline.clickTasksTab();
|
|
20
|
+
}
|
|
21
|
+
await pipeline.waitForTasksReady();
|
|
22
|
+
},
|
|
23
|
+
async back(page) {
|
|
24
|
+
const pipeline = new PipelinePageModel(page);
|
|
25
|
+
await pipeline.clickPipelineTab();
|
|
26
|
+
await pipeline.waitForPipelineSpinner();
|
|
27
|
+
},
|
|
28
|
+
repeat: () => 1,
|
|
29
|
+
thresholds: {
|
|
30
|
+
maxRetainedSizeDeltaBytes: 10 * 1024 * 1024,
|
|
31
|
+
maxNewLeakGroups: 0
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export {
|
|
35
|
+
pipelineTaskNavigationScenario
|
|
36
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Page, Frame } from '@playwright/test';
|
|
2
|
+
import type { ComparisonReport } from '@elliemae/encw-heap-doctor';
|
|
3
|
+
export interface HeapProfilingOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Number of times to execute the flow between the before and after snapshots.
|
|
6
|
+
* More iterations amplify the leak signal — a leak of N bytes per run becomes
|
|
7
|
+
* N × heapIterations in the delta, making small leaks detectable above GC noise.
|
|
8
|
+
* Default: 1.
|
|
9
|
+
*/
|
|
10
|
+
heapIterations?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Label prefix used in snapshot filenames and as the Memento key.
|
|
13
|
+
* Two files are written: `<label>-before-<epoch>.heapsnapshot` and
|
|
14
|
+
* `<label>-after-<epoch>.heapsnapshot`.
|
|
15
|
+
* Default: 'flow'.
|
|
16
|
+
*/
|
|
17
|
+
label?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Number of top leak groups to include in the HeapDoctor report.
|
|
20
|
+
* Default: 5 (HeapDoctor default).
|
|
21
|
+
*/
|
|
22
|
+
topN?: number;
|
|
23
|
+
/**
|
|
24
|
+
* When `true`, throws after comparison if `retainedSizeDelta` exceeds
|
|
25
|
+
* {@link leakThresholdBytes}, causing the Playwright test to fail.
|
|
26
|
+
* Default: false.
|
|
27
|
+
*/
|
|
28
|
+
failOnLeak?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Retained-size growth in bytes above which the test is failed when
|
|
31
|
+
* {@link failOnLeak} is `true`. Set to 0 to fail on any positive delta.
|
|
32
|
+
* Default: 0.
|
|
33
|
+
*/
|
|
34
|
+
leakThresholdBytes?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Optional allow-list of `Window` origins (e.g. `'https://encompass.q3.ice.com'`).
|
|
37
|
+
*
|
|
38
|
+
* Forwarded to `HeapDoctor`; suppresses retainer chains rooted in unrelated
|
|
39
|
+
* same-site iframes (e.g. partner plugins) that share the main V8 isolate.
|
|
40
|
+
* Chains with no `Window` step are always kept.
|
|
41
|
+
*/
|
|
42
|
+
originAllowList?: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
/** Parameters for {@link HeapMemoryProfiler.compare}. */
|
|
45
|
+
export interface CompareParams {
|
|
46
|
+
/** Label passed to `captureSnapshot()` for the baseline. */
|
|
47
|
+
beforeLabel: string;
|
|
48
|
+
/** Label passed to `captureSnapshot()` for the post-flow snapshot. */
|
|
49
|
+
afterLabel: string;
|
|
50
|
+
/** Override for the number of top leak groups in the report. */
|
|
51
|
+
topN?: number;
|
|
52
|
+
/** Allow-list of `Window` origins; forwarded to `HeapDoctor`. */
|
|
53
|
+
originAllowList?: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Encapsulates heap memory profiling for a single Playwright test.
|
|
57
|
+
*
|
|
58
|
+
* **Responsibilities:**
|
|
59
|
+
* - CDP session lifecycle (open, stream chunks, detach)
|
|
60
|
+
* - `.heapsnapshot` file I/O
|
|
61
|
+
* - Snapshot label cache (Memento store, cleared per test)
|
|
62
|
+
* - HeapDoctor comparison and markdown report writing
|
|
63
|
+
* - Optional test failure on leak threshold breach
|
|
64
|
+
*
|
|
65
|
+
* **Usage:**
|
|
66
|
+
* ```ts
|
|
67
|
+
* class MySpec extends AdminBaseTest {
|
|
68
|
+
* private heapProfiler!: HeapMemoryProfiler;
|
|
69
|
+
*
|
|
70
|
+
* async beforeEach() {
|
|
71
|
+
* await super.beforeEach();
|
|
72
|
+
* this.heapProfiler = new HeapMemoryProfiler(this.page, 'reports/heap-snapshots/my-spec');
|
|
73
|
+
* }
|
|
74
|
+
*
|
|
75
|
+
* async afterEach() {
|
|
76
|
+
* this.heapProfiler.clearSnapshots();
|
|
77
|
+
* await super.afterEach();
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* async testFlow_Memory() {
|
|
81
|
+
* await this.goto('some.route');
|
|
82
|
+
* await this.heapProfiler.withProfiling(async () => {
|
|
83
|
+
* await this.somePage.doSomething();
|
|
84
|
+
* }, { heapIterations: 3, label: 'my-flow', failOnLeak: true, leakThresholdBytes: 500_000 });
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* **Non-Chromium:** All methods silently no-op and return `null` / empty string.
|
|
90
|
+
* The flow inside `withProfiling` still executes normally.
|
|
91
|
+
*/
|
|
92
|
+
export declare class HeapMemoryProfiler {
|
|
93
|
+
private readonly page;
|
|
94
|
+
private readonly outputDir;
|
|
95
|
+
/** Memento store: maps snapshot label → absolute file path on disk. */
|
|
96
|
+
private readonly snapshots;
|
|
97
|
+
/**
|
|
98
|
+
* @param {Page} page - The Playwright `Page` instance for the current test.
|
|
99
|
+
* @param {string} outputDir - Directory where `.heapsnapshot` and `.md` files are written.
|
|
100
|
+
* Created automatically if it does not exist.
|
|
101
|
+
*/
|
|
102
|
+
constructor(page: Page, outputDir: string);
|
|
103
|
+
protected getCDPTarget(): Page | Frame;
|
|
104
|
+
private isChromium;
|
|
105
|
+
/**
|
|
106
|
+
* Capture a Chrome heap snapshot via CDP and persist it to disk.
|
|
107
|
+
*
|
|
108
|
+
* The snapshot is streamed in chunks via `HeapProfiler.addHeapSnapshotChunk`,
|
|
109
|
+
* joined, and written as a single `.heapsnapshot` file. The file path is cached
|
|
110
|
+
* internally under `label` so {@link compare} can resolve it by name.
|
|
111
|
+
* @param {string} label - Logical name for this snapshot (e.g. `'before'`, `'after'`).
|
|
112
|
+
* Used as the filename prefix and as the Memento key.
|
|
113
|
+
* @returns {Promise<string>} Absolute path to the written file, or `''` on non-Chromium browsers.
|
|
114
|
+
*/
|
|
115
|
+
captureSnapshot(label: string): Promise<string>;
|
|
116
|
+
/**
|
|
117
|
+
* Compare two previously captured snapshots by their labels.
|
|
118
|
+
*
|
|
119
|
+
* Runs `HeapDoctor.compare()` on the resolved file paths and writes a
|
|
120
|
+
* markdown report to {@link outputDir}.
|
|
121
|
+
* @param {CompareParams} params - {@link CompareParams}
|
|
122
|
+
* @returns {Promise<ComparisonReport | null>} `ComparisonReport`, or `null` on non-Chromium browsers.
|
|
123
|
+
* @throws If either label has no cached snapshot path.
|
|
124
|
+
* @throws If `HeapDoctor.compare` returns `ok: false`.
|
|
125
|
+
*/
|
|
126
|
+
compare(params: CompareParams): Promise<ComparisonReport | null>;
|
|
127
|
+
/**
|
|
128
|
+
* Orchestrate the full heap profiling sequence for a user flow.
|
|
129
|
+
*
|
|
130
|
+
* Sequence (Template Method):
|
|
131
|
+
* 1. Capture **before** snapshot
|
|
132
|
+
* 2. Execute `flow` exactly `heapIterations` times
|
|
133
|
+
* 3. Capture **after** snapshot
|
|
134
|
+
* 4. Run `HeapDoctor.compare` and write the markdown report
|
|
135
|
+
* 5. If `failOnLeak` is `true` and `retainedSizeDelta > leakThresholdBytes`, throw
|
|
136
|
+
*
|
|
137
|
+
* On non-Chromium browsers: `flow` still runs `heapIterations` times,
|
|
138
|
+
* no snapshots are taken, and `null` is returned.
|
|
139
|
+
* @param {() => Promise<void>} flow - Async function containing the user actions to profile.
|
|
140
|
+
* @param {HeapProfilingOptions} [options] - {@link HeapProfilingOptions}
|
|
141
|
+
* @returns {Promise<ComparisonReport | null>} `ComparisonReport` on Chromium, `null` on all other browsers.
|
|
142
|
+
*/
|
|
143
|
+
withProfiling(flow: () => Promise<void>, options?: HeapProfilingOptions): Promise<ComparisonReport | null>;
|
|
144
|
+
/**
|
|
145
|
+
* Clear the internal snapshot label cache.
|
|
146
|
+
* Call in `afterEach` to prevent snapshot paths bleeding across tests.
|
|
147
|
+
*/
|
|
148
|
+
clearSnapshots(): void;
|
|
149
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HeapMemoryProfiler } from '@elliemae/smoked-suite';
|
|
2
1
|
import type { Page, Frame } from '@playwright/test';
|
|
2
|
+
import { HeapMemoryProfiler } from './heapMemoryProfiler.js';
|
|
3
3
|
export declare class IframeHeapProfiler extends HeapMemoryProfiler {
|
|
4
4
|
private readonly profiledPage;
|
|
5
5
|
private readonly frame;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|