Skip to content

feat(parallel): per-VU iteration data for customParallelIterations - #1559

Open
saialekhya-001 wants to merge 15 commits into
developfrom
feat/per-vu-iteration-data
Open

feat(parallel): per-VU iteration data for customParallelIterations#1559
saialekhya-001 wants to merge 15 commits into
developfrom
feat/per-vu-iteration-data

Conversation

@saialekhya-001

Copy link
Copy Markdown

Supersedes #1557 — that PR was auto-closed when its off-convention head branch saialekhya/per-vu-parallel-iterations was renamed to feat/per-vu-iteration-data. Same commits, plus the perPartitionCookieJar fix below. Review history is on #1557.

What

Adds per-VU iteration data + counter for the customParallelIterations run mode, so each virtual user (VU) sees its own iteration index and its own row via pm.iterationData when iterations run in parallel across partitions. This is the routing the @postman/performance-test library depends on for datasets/datafiles as iteration data in parallel perf runs.

Changes

  • Per-VU iteration counter — each partition/VU tracks its own iteration index (partition-manager.js, partition.js, run.js).
  • Per-VU iteration data → pm.iterationData — in custom mode the host-injected per-VU row is seeded into BOTH the VU's _variables (so {{key}} / pm.variables.get() and per-partition markers resolve) AND partition.iterationData, which parallel.command sources the item payload data from → pm.iterationData.get() works. No sandbox change required.
  • Parallel command routing — end-of-partition gating, CR block guards, sentinel cycles, EOF trigger normalization.
  • Full-fresh-on-reuse + late-write drop; settle run-completion callback in custom mode.
  • postman-sandbox — uses published 6.7.3 (has the cycles -1 → iterationCount: Infinity sentinel + pm.datasets streaming); the temporary vendored 6.7.2-per-vu fork was dropped.
  • Tests: unit (custom-parallel-iterations, partition-manager, per-partition-cookie-jar) + integration (customParallelIterations.test.js, datasets.test.js). Full unit + integration suite green locally.

Generated with Claude Code

scriptonist and others added 12 commits July 31, 2026 15:04
- Add Run.isCustomParallelIterations as the single source of truth for
  the mode gate (Change 0)
- Initialize Partition.loopIteration=0 and Partition.stopped=false
  (Change 1, 6c prep)
- Raise cursor.cycles to MAX_SAFE_INTEGER in custom mode so seek
  doesn't trip the bounds guard (Change 2)
- Increment partition.cursor.iteration on each runSinglePartition call
  in custom mode (Change 3)
- Expose Partition.resetVariables() public helper for the full-fresh-
  on-reuse contract landing in a later change (Change 6b prep)

Tests: 12 new unit cases in test/unit/custom-parallel-iterations.test.js
covering construction-time invariants, the Run gating helper, and
multi-loop counter monotonicity. All 449 unit tests pass.
- Change 4: gate the end-of-partition early-return at parallel.command:103
  on !isCustomParallelIterations. Without this, loop 2+ would short-
  circuit (coords.iteration === startIndex + partitionCycles is true
  every loop in custom mode because startIndex=0, partitionCycles=1)
  and the runtime would skip every item.
- Change 5: in the coords.cr block, after firing iteration trigger,
  early-return next() in custom mode. The host (perftest) drives the
  next loop via startParallelIteration; the runtime must not auto-loop
  in parallel. beforeIteration also no longer fires here in custom
  mode — startParallelIteration's own parallel-command invocation
  carries start:true and fires it.

Tests: 4 new unit cases on the parallel processor (with mocked
context) — Change 4 in custom + runtime-managed modes, Change 5 in
custom + runtime-managed modes. 453 passing (was 449).
- Change 6: stopSinglePartition resets partition.loopIteration to 0,
  re-clones partition.variables via the resetVariables() helper, and
  sets partition.stopped=true. Recycled partitions behave as brand-new
  VUs — pm.info.iteration === 0 with a fresh pm.variables scope. Gated
  on isCustomParallelIterations so runtime-managed mode is untouched.
- Change 6c: updatePartitionVariables drops writes when
  partition.stopped is true. Guards the late-write race where a
  script in flight at stopSinglePartition time finishes after the
  variables re-clone, otherwise leaking the dead VU's pm.variables
  mutations into the next VU's fresh scope.

Tests: 9 new unit cases — loopIteration reset, stopped flag toggle,
variables re-clone, flag clear on next runSinglePartition, full-cycle
regression (stop→start→counter=0), runtime-managed-mode non-mutation,
late-write drop happy + race + missing-partition paths. 462 unit tests
passing (was 453).
…7, 9)

- Change 7: at the host.execute call site in event.command.js,
  transform cursor.cycles to -1 when isCustomParallelIterations is
  true. -1 is the wire sentinel that survives JSON/structured-clone
  encoding (Infinity would JSON.stringify to null,
  Number.MAX_SAFE_INTEGER would leak to pm.info.iterationCount).
  Sandbox-side rendering of -1 -> Infinity is the matching change in
  postman-sandbox/lib/sandbox/pmapi.js. Helper factored out as
  applySandboxCursorSentinel and exposed for unit tests.
- Change 9: in the eof branch of parallel.command, fire the iteration
  trigger with payload.coords ('loop just completed') in custom mode
  rather than the post-rollover snapshot. Gated to custom mode to
  preserve existing Newman/desktop semantics — flipping the global
  contract risks silent downstream regressions we can't audit from
  inside postman-runtime.

Tests: 5 new unit cases — sentinel transform happy + non-mutation +
no-op paths, eof trigger custom + runtime-managed payloads.
467 unit tests passing (was 462).
…ions

Self-contained driver that exercises the perftest invocation pattern:
runner.run() -> run.start() -> startParallelIteration() loop until
maxLoops -> abort. Verifies the full chain: runtime drives the loop ->
host.execute hands the (sentinel-transformed) cursor to the sandbox ->
script reads pm.info.iteration + pm.info.iterationCount.

Tests cover:
- T1: first loop sees iteration=0 (via runtime trigger cursor)
- T3: monotonic iteration across 3 loops ([0, 1, 2])
- T2/loop-2-runs-all-items: no skip-everything regression
- T4/T5 trigger counts: iteration + beforeIteration fire once per loop
- T6/T6b: stop+restart resets cursor.iteration to 0 AND re-clones
  pm.variables (verified via in-script marker)
- T10 regression: maxConcurrency=2 mode still completes normally
- T12: pm.info.iterationCount === Infinity end-to-end (cross-repo
  wire contract verified)

Note: T12 requires the matching postman-sandbox change (cycles===-1
rendered as Infinity in pmapi.js). Until the sandbox version pinned
in package.json is bumped, T12 fails locally — run
'node npm/cache.js' in the local postman-sandbox repo and copy
.cache/bootcode.js into node_modules/postman-sandbox/.cache/ to
verify locally before release.
Lint: use object spread instead of Object.assign, repo noop convention
for empty test callbacks, and wrap/format long lines per style rules.

The per-partition cookie jar unit test mocked only the raw
customParallelIterations option, so after stopSinglePartition migrated
to the derived isCustomParallelIterations flag the custom-mode branch
never executed. Mock now mirrors real Run construction; assertions
unchanged.
Custom-mode runs completed via triggerStopAction -> triggers(null)
without settling the stored _process completion callback, leaving the
3-minute global timeout armed, which later re-fired the host callback
with a spurious timeout error. triggerStopAction now routes completion
through the stored process callback (clearing it first) so the
timeout-settling wrapper runs; runtime-managed mode is unaffected.

Integration tests: gate the pm.info.iterationCount assertion on
sandbox capability - exact -1 (sentinel passthrough) with current
postman-sandbox, exact Infinity once the sandbox-side transform ships;
add once-guards around mocha callbacks. Add unit coverage for the
custom-mode completion callback path.
Temporarily bundle postman-sandbox 6.7.2-per-vu.0 (branch
feat/per-vu-variables-parallel-iterations, commit 75d4caf) as a file:
dependency so CI exercises the cycles -1 -> pm.info.iterationCount
Infinity transform before it ships upstream. The capability-gated
integration assertion now runs its Infinity branch.

Re-point to the published postman-sandbox release and drop vendor/
once the sandbox change ships (see vendor/README.md).
vendor/ is npm-ignored so the temporary vendored postman-sandbox
tarball can never ship in a published runtime, and the repository
system test exempts only the exact vendored file: spec from the
exact-semver dependency rule. Both changes are temporary and listed
in the vendor/README.md removal checklist for when the sandbox change
ships upstream.
…vu fork

postman-sandbox 6.7.3 now ships both the per-VU sentinel (cycles -1 ->
pm.info.iterationCount: Infinity, commit 75d4caf) and the pm.datasets
streaming pull-protocol, so the temporary vendored `6.7.2-per-vu.0` tarball
is obsolete. It also lacked the streaming support, which caused the 4
datasets.test.js failures once this branch picked up develop's streaming
tests (#1553).

- point postman-sandbox at published 6.7.3, regenerate lockfile
- drop vendor/ (tgz + README) and the .npmignore vendor/ exclusion
- revert the repository.test.js exact-semver exemption for the file: spec

Validated locally: datasets.test.js 22 passing, customParallelIterations
integration 8 passing (+30 unit), system + lint green.

Co-Authored-By: Claude <noreply@anthropic.com>
In customParallelIterations mode the host injects each VU's data row via
startParallelIteration(index, row). That row was assigned to
partition.variables._variables and sent over the sandbox boundary as
`_variables`, so it surfaced only as pm.variables / {{key}} — pm.iterationData
(which the sandbox builds from the `data` field) stayed empty.

Route the per-VU row into the `data` payload field: partition-manager stores it
as partition.iterationData in custom mode (leaving _variables as the pristine
per-VU script scope), and parallel.command sources the item payload `data` from
partition.iterationData. The sandbox layers iterationData into _variables, so
{{key}} and pm.variables.get() keep resolving and pm.iterationData.get() now
works. No sandbox change required.

This is the routing the @postman/performance-test lib depends on (it was only
ever in the vendored pr1555.0 tarball, never committed); required for datasets/
datafiles as iteration data in parallel perf runs.

Co-Authored-By: Claude <noreply@anthropic.com>
The previous commit routed the per-VU payload to iterationData INSTEAD of
_variables in customParallelIterations mode, which broke perPartitionCookieJar
(the per-partition {{marker}} no longer resolved — 7 failing). Seed both: keep
_variables so {{key}} / pm.variables.get() and per-partition markers resolve
(the original, proven behavior), and additionally set iterationData so the row
surfaces as pm.iterationData.

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.83%. Comparing base (9c2d10d) to head (2557674).

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1559      +/-   ##
===========================================
+ Coverage    77.54%   77.83%   +0.28%     
===========================================
  Files           51       51              
  Lines         4338     4380      +42     
  Branches      1223     1238      +15     
===========================================
+ Hits          3364     3409      +45     
+ Misses         731      729       -2     
+ Partials       243      242       -1     
Flag Coverage Δ
integration 68.85% <78.57%> (+0.14%) ⬆️
legacy 34.08% <7.14%> (-0.24%) ⬇️
unit 47.92% <90.00%> (+1.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread lib/runner/partition.js
Comment thread lib/runner/partition-manager.js
saialekhya-001 and others added 2 commits August 7, 2026 10:29
…ard late writes

Addresses two correctness issues in customParallelIterations reuse:

1. Variable leak across VU reuse. Completed scripts wrote their _variables
   back to the run-global state._variables unconditionally; Partition#_cloneVariables
   clones from that scope on reuse, so a recycled VU inherited the previous
   occupant's mutations. Skip the run-global write in parallel mode
   (event.command.js) — the per-VU scope is persisted via updatePartitionVariables,
   keeping state._variables the pristine baseline new/recycled VUs clone from.

2. Completion race on immediate slot reuse. `stopped` is cleared before the new
   VU's work, so an in-flight completion from the previous occupant could
   overwrite the new occupant. Tag each partition with a generation (bumped on
   stopSinglePartition), stamp it onto the queued execution (coords.partitionGeneration),
   and drop writes in updatePartitionVariables whose generation no longer matches.

Tests: unit coverage for the generation guard, custom-mode reuse (iterationData +
_variables routing, cursor.iteration bump), and the poolFinished abort/error
branches; an integration regression proving a recycled VU sees a pristine scope
(fails without fix 1). Full lint + unit green; targeted integration green.

Co-Authored-By: Claude <noreply@anthropic.com>
Lint-only follow-up to the per-VU reuse fix (CI Lint job):
- parallel.command.js: use `{ ...coords, partitionGeneration }` instead of Object.assign
- partition-manager.js: document the new `generation` param on updatePartitionVariables

Co-Authored-By: Claude <noreply@anthropic.com>
@saialekhya-001

Copy link
Copy Markdown
Author

CI note: codecov/project/legacy is pre-existing and non-blocking

All the checks that matter are green — Lint, the full Tests matrix (18/20/latest × ubuntu/win), Browser Tests, codecov/patch, codecov/project, and the unit + integration project flags.

The only red check is codecov/project/legacy (34.09% vs 35% target), which is not introduced by this PR:

  • It is already failing on develop — base (16cdd37) legacy coverage is 34.33%, i.e. below the 35% target regardless of this PR.
  • This PR's code is well covered: unit flag <90.00%>, integration flag <78.57%> on the diff. The dip is only on the legacy flag (<7.14%>) because the "legacy" suite is the old callback-style test-integration-legacy tests, which don't exercise a new customParallelIterations feature (adding legacy-suite tests for a brand-new parallel mode would be contrived).
  • It is not a required/blocking check — the PR is mergeable; BLOCKED reflects the pending review, not this status.

Suggested resolution (repo-owner call, out of scope for this PR): either lower the legacy target in codecov.yml to match actual repo coverage, or backfill legacy-suite coverage as a separate hygiene task.

@saialekhya-001

Copy link
Copy Markdown
Author

Behaviour in customParallelIterations mode (perf runs)

Quick reference for how the iteration/data primitives behave under this PR. A perf run isn't "N iterations over a data file" — it's V virtual users (VUs), each a partition, looping for the whole duration; each loop the host hands the VU one data row (via the distribution strategy).

pm.info.iteration — each VU's own loop counter, 0-indexed, climbing for the whole run (VU-A on its 5th loop reads 4 while VU-B on its 2nd reads 1). Independent per VU, not a shared global count. Resets to 0 when a VU slot is recycled (full-fresh-on-reuse).

pm.info.iterationCountInfinity. There's no fixed count; the run ends by duration. (Internally sent as the -1 wire sentinel and rendered as Infinity by postman-sandbox ≥ 6.7.3, which this PR uses.) Scripts should not gate logic on iterationCount in this mode — use pm.info.iteration for a per-VU counter.

pm.iterationData — the VU's current row for this loop (the dataset/datafile row picked by the distribution strategy). Read-only. pm.iterationData.get('col') returns the row's value. (This is what the PR routes into the item payload's data field so the sandbox exposes it.)

pm.variables — the same row is also seeded into the VU's Local scope, so {{col}} and pm.variables.get('col') resolve it too. pm.variables.set(...) writes to that VU's own Local scope — isolated per VU (a recycled VU does not inherit a previous occupant's set() values). pm.variables never mutates pm.iterationData.

Resolution precedence for pm.variables.get() is unchanged: Local → Data (iterationData) → Environment → Collection → Globals.

Normal (functional) runs are unaffected: pm.info.iteration is 0…N-1, iterationCount is the real total, and pm.iterationData is data[iteration] as before.

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