Skip to content

feat: testplane profiler - #1314

Merged
KuznetsovRoman merged 36 commits into
masterfrom
TESTPLANE-1087.profiler
Aug 25, 2026
Merged

feat: testplane profiler#1314
KuznetsovRoman merged 36 commits into
masterfrom
TESTPLANE-1087.profiler

Conversation

@KuznetsovRoman

Copy link
Copy Markdown
Member

No description provided.

@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/gemini-testing/testplane@1314

commit: 5f66820

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

✅ Testplane E2E run succeed

Report

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

✅ Testplane browser-env run succeed

Report

Comment thread src/profiler/schema/v1.ts
configuredSessionsPerBrowser?: Record<string, number>;
}

export interface ProfilerResultV1 {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This object is emitted as event and saved to file

collector: bucket,
seen: state.seen,
retained,
rule: `top ${RETENTION_POLICY_V1.operationLimits[bucket]} by wall time; keep ancestors of retained operations`,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules, reasons, evidence, confidence and such are saved as human-readable values so we could display clear strings in profiler UI with at least as possible logic

@@ -0,0 +1,49 @@
import type { EnabledProfilerLevel } from "../schema";

export const RETENTION_POLICY_V1 = Object.freeze({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object configures "how much slowest objects of each type we should keep"

evicted?: T;
}

export class TopK<T> {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classic min-heap topK implementation on array
The least element is stored at the root

private _max = -Infinity;
private _mean = 0;
private _m2 = 0;
private readonly _reservoir: number[] = [];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reservoir Sampling for percentile readings


this._socket.on(BrowserEventNames.runBrowserCommand, this._handleRunBrowserCommand(browser));
this._socket.on(BrowserEventNames.profilerFragment, payload => {
this._profiler.recordMeasurement("browser.runnable", payload.wallMs, {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recording somewhat same values on browser env, as we do on node.js runtime, but it lacks:

  • detailed hook spans
  • test body
  • commands
  • event loop utilization/delay
  • CPU
    We also have just one browser runnable here, which would just help to decide between "slow test" and "slow transport"

Comment thread src/utils/exit-code.ts

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After testplane profiler finalization, process might already have error code, but then get to another process.exit, which messes up exit code.
In this module we calculate origin exit code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In "profile CLI commands" we wrap other CLI commands to same profiler flow

Comment thread src/config/options.js
}),
}),

profiler: section({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Describe profiler root section

Comment thread src/config/defaults.js
input: ".testplane/failed.json",
},
profiler: {
level: 0,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabled by default in order to not waste extra CPU cycles in CI

@KuznetsovRoman

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35bab54a27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/profiler/manager.ts

private async _deliverResult(result: Readonly<ProfilerResultV1>): Promise<void> {
try {
printProfilerResult(result, this._console);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep profiler summaries off stdout for data commands

When profiler.level is enabled for data-producing CLI commands such as config, list-browsers, or list-tests, those commands still write their JSON/plain result to stdout inside the profiled action, and this unconditional finalization step appends the human-readable profiler report to the same stream. That makes otherwise successful command output invalid for consumers that parse stdout as JSON; route the profiler summary to stderr or suppress it for these commands unless explicitly requested.

Useful? React with 👍 / 👎.

@DudaGod DudaGod left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding info about new feature to docs

);

if (browsersInstallPerStatus[BrowserInstallStatus.Error].length) {
process.exitCode = resolveExitCode(1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like here callback ends successfully and return undefined in action inside ProfilerManager is this expected behavior? In this case runOutcome would be passed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in latest commit "fix: profile install dependency failures"

<script type="module" src="${MODULE_NAMES.mocha}"></script>
<script type="module" src="${MODULE_NAMES.browserRunner}"></script>
<script type="module" src="${viteFsUrl(modules.globals)}"></script>
<script src="${MOCHA_RUNTIME_URL}"></script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we move vite module loading with @testplane/mocha to script /__testplane__/mocha.js ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug: Relative browser-env runtime URLs were resolved under /run-uuids/, causing Vite’s HTML fallback to return an error page instead of runtime JavaScript, which resulted in "Browser didn't connect to the Vite server in 10000ms"

The bug predates the profiler and was only discovered while browser runtime telemetry increased browser-env coverage.

Runtime ESM files now use rooted /@fs URLs, Mocha is served as a classic UMD script, route parsing is stricter, and the Testplane package root is allowed by Vite, which fixes the issue.

await fs.mkdir(directory, { recursive: true });
try {
await fs.writeFile(temporary, `${JSON.stringify(result, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
await fs.rename(temporary, destination);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using public api we can call readTests() with configured profiler and then run method. Looks like in this case I will get only profiler with the last call. Is this intentionally behavior?

Canonical ProfilerResultV1 DTO and schema exports for internal consumers.
Internal profiler level and configuration types without enabling the public option.
Bounded top-K storage with parent-safe retention and focused tests.
Bounded streaming moments, reservoirs, percentiles, weighted merge, and focused tests.
Runtime interfaces, strict no-op path, clocks, async activity tracking, and startup probe.
Span lifecycle, ALS context, retention interaction, and child interval union with matching tests.
Aggregate and metric updates plus bounded resource sampling with matching tests.
Fragment deltas, runtime-owned cloning, hostile input validation, and matching tests.
Source-scoped ordering, deduplication, gaps, pending caps, and matching tests.
Synthetic root, parent repair, timing formulas, compact references, and their behavioral tests.
Versioned thresholds, robust outlier detection, and deterministic finding order.
Lifecycle, major-phase, discovery, and unattributed-time analyzers with matching tests.
File, module, and listener analyzers with their positive and negative tests.
Slow-test, hook, and browser-command analyzers with matching tests.
Worker, session, CPU, and event-loop resource analyzers with matching tests.
Descriptor-safe redaction and structural sanitization for every public delivery path.
Serialized timeline and whole-result budgets with explicit truncation fallbacks.
Optional JSON delivery through a unique temporary file and atomic rename.
Grouped ANSI-aware presentation emitted through one logger call with renderer tests.
One-result guard, finalization pipeline, immutable payload, and fail-open deliveries with tests.
Node-compatible listener observation while preserving registration, removal, and emission semantics.
Translate event observer callbacks into profiler envelopes and individual listener spans.
Initialize config observation and a dormant no-op-compatible manager.
Composable CommonJS and ESM module observation shared with selectivity, with regression tests.
Profile browser preparation and command execution with bounded safe attributes and focused tests.
Pool queue, limiter state, acquisition, release, and session lifecycle observation with tests.
Master attempts and worker hook, body, cleanup, and retry spans with execution tests.
Discovery, parsing, module scopes, cache waits, and module-graph collection with tests.
Process messages, master ingestion, flush ordering, late fragments, and registry tests.
Worker clock handshake, facade RPC, fragment batching, and worker-side tests.
Propagate the optional runtime through master and worker runner boundaries.
Bounded runnable, resource, and long-task telemetry relayed across the browser environment.
Add top-level operations, teardown, signals, and exit-code preservation.
Early CLI bootstrap and command wrappers with preserved success and failure exit semantics.
Enable public config, result delivery, exports, and end-to-end tests.
@KuznetsovRoman
KuznetsovRoman force-pushed the TESTPLANE-1087.profiler branch 2 times, most recently from 4fb24c4 to 5f66820 Compare August 25, 2026 01:20
@KuznetsovRoman
KuznetsovRoman merged commit 875031c into master Aug 25, 2026
9 checks passed
@KuznetsovRoman
KuznetsovRoman deleted the TESTPLANE-1087.profiler branch August 25, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants