diff --git a/docs/reference/config/main.mdx b/docs/reference/config/main.mdx
index 52bdf120..b405939f 100644
--- a/docs/reference/config/main.mdx
+++ b/docs/reference/config/main.mdx
@@ -82,6 +82,13 @@ All [browser settings][browsers] (except `desiredCapabilities`) can be moved to
[`lastFailed`][last-failed] |
Section for configuring the rerun of only failed tests. |
+
+ | [`profiler`][profiler] |
+
+ Section for collecting a versioned performance profile of a Testplane run and
+ identifying likely bottlenecks.
+ |
+
@@ -92,6 +99,7 @@ Follow the link or select the desired section in the left navigation menu of the
[system]: ../system
[plugins]: ../plugins
[last-failed]: ../last-failed
+[profiler]: ../profiler
[dev-server]: ../dev-server
[prepare-browser]: ../prepare-browser
[prepare-environment]: ../prepare-environment
diff --git a/docs/reference/config/profiler.mdx b/docs/reference/config/profiler.mdx
new file mode 100644
index 00000000..2c77afeb
--- /dev/null
+++ b/docs/reference/config/profiler.mdx
@@ -0,0 +1,156 @@
+# profiler
+
+## Overview {/* #overview */}
+
+The built-in profiler explains where a Testplane run spent wall time and CPU, highlights likely bottlenecks, and suggests changes to try. Measurements and recommendations are kept separate: every recommendation includes evidence and a `high`, `medium`, or `low` confidence level.
+
+Profiling works for CLI runs and the programmatic `run` and `readTests` APIs.
+
+## Setup {/* #setup */}
+
+```javascript title="testplane.config.js"
+module.exports = {
+ profiler: {
+ level: 2,
+ output: "profiler-result.json",
+ },
+};
+```
+
+| Parameter | Type | Default | Description |
+| ------------------- | ------------------ | ------- | ------------------------------------------------------------------------------------ |
+| [`level`](#level) | `0 \| 1 \| 2 \| 3` | `0` | Selects cumulative collection detail. `0` disables the profiler. |
+| [`output`](#output) | `string \| null` | `null` | Optional path to an atomic JSON report, resolved from the current working directory. |
+
+### level {/* #level */}
+
+The levels are cumulative:
+
+- `0` collects nothing and produces no profiler console output, event, or file;
+- `1` records the full run and major lifecycle phases, process CPU, event-loop metrics, memory, and host CPU samples;
+- `2` adds event listeners, test-file loading and cache behavior, tests and grouped hooks, worker utilization, browser-pool queues, and session reuse;
+- `3` adds individual hooks, browser commands, CommonJS/ESM load boundaries, scoped async active/waiting time, and partial browser-runtime telemetry.
+
+Use level 1 to locate a slow phase, level 2 for normal investigation, and level 3 only when the additional detail is needed. `level` must be an integer from 0 through 3.
+
+### output {/* #output */}
+
+When set, `output` must be a non-empty path ending in `.json`. Testplane writes the report through a temporary file and atomically renames it. Without `output`, the console summary and [`PROFILER_RESULT`](../../testplane-events#profiler_result) event remain available.
+
+TypeScript users can import the public result type from the package root:
+
+```typescript
+import type { ProfilerResultV1 } from "testplane";
+```
+
+## Reading the result {/* #reading_the_result */}
+
+The report has these top-level sections:
+
+- `run`: operation, outcome, total duration, and partial-result reasons;
+- `environment` and `capabilities`: runtime details and which measurements were available;
+- `timeline`: retained operations with process and correlation context;
+- `aggregates`: full streaming statistics, even when detailed operations were truncated;
+- `findings`: evidence-backed observations and suggested experiments;
+- `dataQuality`: collector coverage, clock uncertainty, and warnings;
+- `profiler`: bounded collection errors, truncation information, and measured in-run profiler overhead.
+
+A console result is formatted as a readable report with the execution breakdown, detailed findings, and suggested actions:
+
+```text
+[profiler] Test run profile
+________________________________________________________________________________________
+
+Total time: 452ms
+
+Execution breakdown
+
+ Phase Time Time % Bar
+ ____________________________ _____ ______ __________
+ Initialize Testplane 351ms 77.8% ██████████
+ Discover and load test files 85ms 18.7% ██
+ Load configuration 7ms 1.5%
+ Unattributed 7ms 1.5%
+ Load plugins 2ms 0.4%
+ Set up transforms 0ms <0.1%
+
+Performance findings
+
+1. MEDIUM • Slow event listener • init:acceptanceSlowInit
+
+ init:acceptanceSlowInit used 351ms across 1 call(s). Slowest retained call at
+ /path/to/project/.profiler-acceptance/acceptance-plugin.cjs
+ (.profiler-acceptance/acceptance-plugin.cjs:5:19) took 351ms.
+
+ Slowest call breakdown
+ Activity Time Call % Bar
+ _________ _____ ______ _____________
+ Active JS 0ms <0.1%
+ Waiting 350ms 99.9% █████████████
+
+ Suggested action:
+ init:acceptanceSlowInit (acceptance-plugin.cjs:5:19): waiting dominates the slowest
+ retained call; inspect awaited I/O or timers and remove avoidable serial waits.
+
+________________________________________________________________________________________
+[profiler] 1 finding: 1 medium
+```
+
+The JSON keeps measurements and advice separate:
+
+```json
+{
+ "schemaVersion": 1,
+ "run": {
+ "level": 2,
+ "profileStatus": "complete",
+ "runOutcome": "passed",
+ "durationMs": 133000
+ },
+ "timeline": [],
+ "aggregates": {},
+ "findings": [
+ {
+ "category": "event-listener",
+ "confidence": "high",
+ "evidence": [{ "metric": "wall", "value": 30000, "unit": "ms" }],
+ "action": "Inspect this listener's source and reduce synchronous work."
+ }
+ ]
+}
+```
+
+The example is abbreviated. Use the public `ProfilerResultV1` type and `schemaVersion` when consuming the complete payload.
+
+Each timeline operation distinguishes wall time from cumulative work, overlap, critical-path impact, and the available CPU estimate. Parallel operations can have cumulative work greater than the run wall time; this is expected and must not be read as elapsed run duration.
+
+`processCpuMs` is a process-window measurement and is not exclusive when operations overlap. Level 3 may additionally provide thread CPU and estimates of synchronous JS activity versus asynchronous waiting. Event-loop delay is process-wide. Browser CPU attribution is not available in v1; consult `capabilities` and `dataQuality.coverage` before relying on any optional field.
+
+Entity details are retained with deterministic top-K and serialized-size limits. `profiler.truncation` states what was seen and retained; aggregates still include all observations. Internal collector failures make the result `partial` but do not change the test outcome.
+
+Paths are project-relative when possible, URLs have credentials/query/hash removed, and known secret-like values are redacted. Raw browser session IDs and raw browser-command arguments are not included.
+
+## Lifecycle boundary {/* #lifecycle_boundary */}
+
+The profile starts at CLI/API entry, includes configuration, plugins, initialization, test discovery and loading, master/worker startup, sessions, test execution, reporters, and normal teardown. Stages that a command does not execute are absent rather than reported with zero duration. Uncovered time is represented as `unattributed`.
+
+The final snapshot is frozen after teardown and then delivered to the console, the event, and the optional JSON file. Snapshot creation, serialization, and event delivery are profiler overhead, but cannot recursively appear inside the already frozen payload. Output or event-handler failures are reported as warnings and do not change the test result.
+
+```text
+END → RUNNER_END → worker flush/shutdown → afterAll and cleanup
+ → freeze ProfilerResultV1 → console + PROFILER_RESULT + optional JSON
+```
+
+On graceful termination, Testplane requests a bounded worker flush and emits a partial profile with the abort reason. A second termination signal keeps the existing force-exit behavior, so delivery cannot be guaranteed in that case.
+
+## Consuming the event {/* #consuming_the_event */}
+
+```javascript
+module.exports = testplane => {
+ testplane.on(testplane.events.PROFILER_RESULT, async result => {
+ await sendToTelemetry(result);
+ });
+};
+```
+
+The async event receives the same immutable object used for the console and JSON outputs. Its own handlers are outside the frozen profile and cannot recursively add spans to it.
diff --git a/docs/reference/testplane-events.mdx b/docs/reference/testplane-events.mdx
index 82d00d6a..9072efcc 100644
--- a/docs/reference/testplane-events.mdx
+++ b/docs/reference/testplane-events.mdx
@@ -107,7 +107,7 @@ Then everything will depend on the result of the test run. If the test passed su
If the test does not need to be re-run, and the result is final, Testplane triggers the [TEST_END](#test_end) and [SUITE_END](#suite_end) events if it refers to the completion of a describe-block.
-After all tests have been executed and sessions completed, Testplane triggers the [END](#end) and [RUNNER_END](#runner_end) events.
+After all tests have been executed and sessions completed, Testplane triggers the [END](#end) and [RUNNER_END](#runner_end) events. When the built-in profiler is enabled, Testplane completes normal teardown and then triggers [PROFILER_RESULT](#profiler_result) with the final immutable profile.
#### Updating reference screenshots
@@ -526,7 +526,9 @@ testplane.on(testplane.events.BEFORE_FILE_READ, ({ file, testParser }) => {
testParser.setController("logger", {
log: function (prefix) {
console.log(
- `${prefix}: just parsed ${this.fullTitle()} from file ${file} for browser ${this.browserId}`,
+ `${prefix}: just parsed ${this.fullTitle()} from file ${file} for browser ${
+ this.browserId
+ }`,
);
},
});
@@ -715,6 +717,28 @@ The event handler receives an object with the test run statistics in the followi
See the example [above](#runner_start_usage) about opening and closing the tunnel when the runner starts and stops.
+## PROFILER_RESULT {/* #profiler_result */}
+
+**async | master**
+
+The `PROFILER_RESULT` event is triggered once after normal teardown when the built-in [profiler](../config/profiler) is enabled. It is also delivered for a partial result when Testplane can finalize an aborted or failed operation. Level 0 does not trigger the event.
+
+The final result is frozen before delivery. The event handler itself is outside the measured timeline, so it cannot recursively change the profile. A rejected handler is reported as a profiler delivery warning and does not change the test result.
+
+### Subscribing to the event {/* #profiler_result_subscription */}
+
+```javascript
+testplane.on(testplane.events.PROFILER_RESULT, async result => {
+ console.info(`Profile ${result.run.id}: ${result.run.durationMs} ms`);
+});
+```
+
+#### Handler parameters {/* #profiler_result_cb_params */}
+
+The handler receives a readonly `ProfilerResultV1` object. It contains the run metadata, environment and capabilities, retained timeline, full aggregates, evidence-backed findings, data-quality information, collection errors, and truncation metadata. The same object is used for the console summary and optional JSON output.
+
+See the [profiler configuration reference](../config/profiler#reading_the_result) for timing semantics, data-quality rules, result fields, and privacy boundaries.
+
## NEW_WORKER_PROCESS {/* #new_worker_process */}
**sync | master**
@@ -846,7 +870,10 @@ module.exports = (testplane, opts) => {
// pluginConfig.browserWSEndpoint defines a function that should return the URL
// for working with the browser via CDP. To allow the function to compute the URL,
// the function receives the session identifier and the grid URL
- const browserWSEndpoint = pluginConfig.browserWSEndpoint({ sessionId, gridUrl });
+ const browserWSEndpoint = pluginConfig.browserWSEndpoint({
+ sessionId,
+ gridUrl,
+ });
const devtools = await DevTools.create({ browserWSEndpoint });
diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/main.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/main.mdx
index ec827885..faeeb9da 100644
--- a/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/main.mdx
+++ b/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/main.mdx
@@ -79,6 +79,13 @@ import ConfigExample from "@site/docs/reference/config/_partials/examples/_confi
[`lastFailed`][last-failed] |
Раздел для конфигурирования перезапуска только упавших тестов. |
+
+ | [`profiler`][profiler] |
+
+ Раздел для сбора версионированного профиля производительности прогона Testplane и
+ выявления вероятных узких мест.
+ |
+
@@ -89,6 +96,7 @@ import ConfigExample from "@site/docs/reference/config/_partials/examples/_confi
[system]: ../system
[plugins]: ../plugins
[last-failed]: ../last-failed
+[profiler]: ../profiler
[dev-server]: ../dev-server
[prepare-browser]: ../prepare-browser
[prepare-environment]: ../prepare-environment
diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/profiler.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/profiler.mdx
new file mode 100644
index 00000000..6d622b29
--- /dev/null
+++ b/i18n/ru/docusaurus-plugin-content-docs/current/reference/config/profiler.mdx
@@ -0,0 +1,156 @@
+# profiler
+
+## Обзор {/* #overview */}
+
+Встроенный профайлер показывает, на что во время прогона Testplane ушло фактическое время (wall time) и процессорное время, выделяет вероятные узкие места и предлагает изменения, которые стоит попробовать. Измерения и рекомендации разделены: каждая рекомендация содержит подтверждающие данные и уровень уверенности `high`, `medium` или `low`.
+
+Профилирование поддерживается для запусков через CLI и программных API `run` и `readTests`.
+
+## Настройка {/* #setup */}
+
+```javascript title="testplane.config.js"
+module.exports = {
+ profiler: {
+ level: 2,
+ output: "profiler-result.json",
+ },
+};
+```
+
+| Параметр | Тип | По умолчанию | Описание |
+| ------------------- | ------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------- |
+| [`level`](#level) | `0 \| 1 \| 2 \| 3` | `0` | Определяет совокупную детализацию сбора данных. `0` отключает профайлер. |
+| [`output`](#output) | `string \| null` | `null` | Необязательный путь к JSON-отчёту, который записывается атомарно и разрешается относительно текущей рабочей директории. |
+
+### level {/* #level */}
+
+Уровни являются накопительными:
+
+- `0` не собирает данные, ничего не выводит в консоль профайлера и не создаёт событие или файл;
+- `1` записывает весь прогон и основные фазы жизненного цикла, процессорное время процесса, метрики цикла событий, память и образцы загрузки CPU хоста;
+- `2` добавляет обработчики событий, загрузку тестовых файлов и работу кэша, тесты и сгруппированные хуки, загрузку воркеров, очереди пула браузеров и повторное использование сессий;
+- `3` добавляет отдельные хуки, браузерные команды, границы загрузки CommonJS/ESM, активное время и время ожидания отдельных асинхронных областей, а также частичную телеметрию браузерного окружения.
+
+Используйте уровень 1 для поиска медленной фазы, уровень 2 для обычного исследования, а уровень 3 — только когда нужна дополнительная детализация. Значение `level` должно быть целым числом от 0 до 3.
+
+### output {/* #output */}
+
+Если параметр задан, `output` должен быть непустым путём с расширением `.json`. Testplane записывает отчёт во временный файл, а затем атомарно переименовывает его. Если `output` не задан, сводка в консоли и событие [`PROFILER_RESULT`](../../testplane-events#profiler_result) остаются доступными.
+
+Пользователи TypeScript могут импортировать публичный тип результата из корня пакета:
+
+```typescript
+import type { ProfilerResultV1 } from "testplane";
+```
+
+## Чтение результата {/* #reading_the_result */}
+
+Отчёт содержит следующие секции верхнего уровня:
+
+- `run`: операция, результат, общая длительность и причины частичного результата;
+- `environment` и `capabilities`: сведения о среде выполнения и доступных измерениях;
+- `timeline`: сохранённые операции с контекстом процесса и корреляции;
+- `aggregates`: полная потоковая статистика, даже если подробные операции были усечены;
+- `findings`: наблюдения, подтверждённые данными, и предлагаемые эксперименты;
+- `dataQuality`: покрытие сборщиков, погрешность синхронизации часов и предупреждения;
+- `profiler`: ограниченный набор ошибок сбора, сведения об усечении и измеренные накладные расходы профайлера во время прогона.
+
+Результат в консоли оформляется как читаемый отчёт с разбивкой времени выполнения, подробными наблюдениями и предлагаемыми действиями:
+
+```text
+[profiler] Test run profile
+________________________________________________________________________________________
+
+Total time: 452ms
+
+Execution breakdown
+
+ Phase Time Time % Bar
+ ____________________________ _____ ______ __________
+ Initialize Testplane 351ms 77.8% ██████████
+ Discover and load test files 85ms 18.7% ██
+ Load configuration 7ms 1.5%
+ Unattributed 7ms 1.5%
+ Load plugins 2ms 0.4%
+ Set up transforms 0ms <0.1%
+
+Performance findings
+
+1. MEDIUM • Slow event listener • init:acceptanceSlowInit
+
+ init:acceptanceSlowInit used 351ms across 1 call(s). Slowest retained call at
+ /path/to/project/.profiler-acceptance/acceptance-plugin.cjs
+ (.profiler-acceptance/acceptance-plugin.cjs:5:19) took 351ms.
+
+ Slowest call breakdown
+ Activity Time Call % Bar
+ _________ _____ ______ _____________
+ Active JS 0ms <0.1%
+ Waiting 350ms 99.9% █████████████
+
+ Suggested action:
+ init:acceptanceSlowInit (acceptance-plugin.cjs:5:19): waiting dominates the slowest
+ retained call; inspect awaited I/O or timers and remove avoidable serial waits.
+
+________________________________________________________________________________________
+[profiler] 1 finding: 1 medium
+```
+
+В JSON измерения и рекомендации хранятся отдельно:
+
+```json
+{
+ "schemaVersion": 1,
+ "run": {
+ "level": 2,
+ "profileStatus": "complete",
+ "runOutcome": "passed",
+ "durationMs": 133000
+ },
+ "timeline": [],
+ "aggregates": {},
+ "findings": [
+ {
+ "category": "event-listener",
+ "confidence": "high",
+ "evidence": [{ "metric": "wall", "value": 30000, "unit": "ms" }],
+ "action": "Inspect this listener's source and reduce synchronous work."
+ }
+ ]
+}
+```
+
+Пример сокращён. Для обработки полного содержимого используйте публичный тип `ProfilerResultV1` и поле `schemaVersion`.
+
+Каждая операция в `timeline` разделяет фактическое время, суммарную работу, перекрытие, влияние на критический путь и доступную оценку CPU. Для параллельных операций суммарная работа может превышать фактическое время всего прогона. Это ожидаемое поведение, и такое значение нельзя интерпретировать как прошедшее время прогона.
+
+`processCpuMs` измеряется для временного окна процесса и не является эксклюзивным при перекрытии операций. На уровне 3 дополнительно могут быть доступны процессорное время потока и оценки синхронной активности JavaScript в сравнении с асинхронным ожиданием. Задержка цикла событий измеряется для всего процесса. В версии 1 атрибуция CPU браузера недоступна. Перед использованием любого необязательного поля проверяйте `capabilities` и `dataQuality.coverage`.
+
+Сведения о сущностях сохраняются с детерминированными ограничениями top-K и сериализованного размера. Поле `profiler.truncation` показывает, сколько данных было обнаружено и сохранено, при этом агрегаты по-прежнему включают все наблюдения. Внутренние ошибки сборщиков переводят результат в состояние `partial`, но не изменяют результат тестов.
+
+Пути по возможности задаются относительно проекта, из URL удаляются учётные данные, строка запроса и hash-фрагмент, а значения, похожие на секреты, маскируются. Исходные идентификаторы браузерных сессий и аргументы браузерных команд не включаются.
+
+## Граница жизненного цикла {/* #lifecycle_boundary */}
+
+Профиль начинается на входе CLI/API и включает конфигурацию, плагины, инициализацию, обнаружение и загрузку тестов, запуск master-процесса и воркеров, сессии, выполнение тестов, репортеры и штатное завершение работы. Этапы, которые команда не выполняет, отсутствуют, а не отображаются с нулевой длительностью. Непокрытое время представлено как `unattributed`.
+
+Итоговый снимок замораживается после завершения работы, а затем передаётся в консоль, событие и необязательный JSON-файл. Создание снимка, сериализация и доставка события относятся к накладным расходам профайлера, но не могут рекурсивно попасть в уже замороженный результат. Ошибки записи результата или обработчика события выводятся как предупреждения и не изменяют результат тестов.
+
+```text
+END → RUNNER_END → сброс данных и завершение воркеров → afterAll и очистка
+ → заморозка ProfilerResultV1 → консоль + PROFILER_RESULT + необязательный JSON
+```
+
+При штатном завершении по сигналу Testplane запрашивает ограниченный по времени сброс данных воркеров и отправляет частичный профиль с причиной прерывания. Второй сигнал завершения сохраняет существующее поведение принудительного выхода, поэтому доставка результата в этом случае не гарантируется.
+
+## Обработка события {/* #consuming_the_event */}
+
+```javascript
+module.exports = testplane => {
+ testplane.on(testplane.events.PROFILER_RESULT, async result => {
+ await sendToTelemetry(result);
+ });
+};
+```
+
+Асинхронное событие получает тот же неизменяемый объект, который используется для вывода в консоль и JSON. Его обработчики находятся за пределами замороженного профиля и не могут рекурсивно добавлять в него интервалы.
diff --git a/i18n/ru/docusaurus-plugin-content-docs/current/reference/testplane-events.mdx b/i18n/ru/docusaurus-plugin-content-docs/current/reference/testplane-events.mdx
index 42dd2966..8b609dea 100644
--- a/i18n/ru/docusaurus-plugin-content-docs/current/reference/testplane-events.mdx
+++ b/i18n/ru/docusaurus-plugin-content-docs/current/reference/testplane-events.mdx
@@ -108,7 +108,7 @@ Testplane можно запускать как через [CLI (командну
Если тест не нужно запускать повторно, и результат — окончательный, то testplane триггерит события [TEST_END](#test_end) и [SUITE_END](#suite_end), если речь идет о завершении выполнения describe-блока.
-После того как все тесты будут выполнены, а сессии завершены, testplane триггерит события [END](#end) и [RUNNER_END](#runner_end).
+После того как все тесты будут выполнены, а сессии завершены, testplane триггерит события [END](#end) и [RUNNER_END](#runner_end). Когда встроенный профайлер включён, Testplane завершает штатную очистку ресурсов, а затем триггерит событие [PROFILER_RESULT](#profiler_result) с итоговым неизменяемым профилем.
#### Обновление эталонных скриншотов
@@ -714,6 +714,28 @@ testplane.on(testplane.events.RUNNER_END, async result => {
Смотрите пример [выше](#runner_start_usage) про открытие и закрытие туннеля при запуске и остановке раннера.
+## PROFILER_RESULT {/* #profiler_result */}
+
+**async | master**
+
+Событие `PROFILER_RESULT` триггерится один раз после штатной очистки ресурсов, когда включён встроенный [профайлер](../config/profiler). Оно также отправляется с частичным результатом, если Testplane может завершить формирование профиля для прерванной или завершившейся ошибкой операции. На уровне 0 событие не триггерится.
+
+Итоговый результат замораживается до отправки. Сам обработчик события находится за пределами измеряемой временной шкалы, поэтому он не может рекурсивно изменить профиль. Отклонённый Promise обработчика регистрируется как предупреждение о доставке результата профайлера и не изменяет результат тестов.
+
+### Подписка на событие {/* #profiler_result_subscription */}
+
+```javascript
+testplane.on(testplane.events.PROFILER_RESULT, async result => {
+ console.info(`Profile ${result.run.id}: ${result.run.durationMs} ms`);
+});
+```
+
+#### Параметры обработчика {/* #profiler_result_cb_params */}
+
+Обработчик получает readonly-объект `ProfilerResultV1`. Он содержит метаданные прогона, сведения о среде и возможностях, сохранённую временную шкалу, полные агрегаты, подтверждённые данными выводы, сведения о качестве данных, ошибки сбора и метаданные усечения. Тот же объект используется для сводки в консоли и необязательного JSON-файла.
+
+Семантика измерения времени, правила качества данных, поля результата и границы приватности описаны в [справке по конфигурации профайлера](../config/profiler#reading_the_result).
+
## NEW_WORKER_PROCESS {/* #new_worker_process */}
**sync | master**