Skip to content

perf(codegen): eliminate per-access array-read guard calls in hot masked-index loops - #6750

Merged
proggeramlug merged 3 commits into
PerryTS:mainfrom
proggeramlug:perf/array-index-guard-hoist
Jul 22, 2026
Merged

perf(codegen): eliminate per-access array-read guard calls in hot masked-index loops#6750
proggeramlug merged 3 commits into
PerryTS:mainfrom
proggeramlug:perf/array-index-guard-hoist

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Array element reads (arr[i]) in tight loops compiled to one runtime guard call per accessjs_typed_feedback_numeric_array_index_get_guard for plain arrays, and out-of-line getters (js_typed_array_get / js_typed_array_index_get_dynamic) for typed arrays. The element load itself was already inline; the per-access call was the tax, making integer/array-heavy JS 10–15× slower than V8 while plain i32 arithmetic was already at parity. bcryptjs's Blowfish S-boxes are exactly this shape (S[x >>> 24], S[0x100 | ((x >> 16) & 0xff)], ~600M reads per cost-10 compareSync).

This PR removes the per-access call for hot masked-index loops via three complementary changes, keeping every cold/deopt case on the existing runtime fallbacks.

1. Static index windows for the buffer bounds prover (expr/range_facts.rs)

int_range_expr now understands the bit-mask index idioms:

  • e & K[0, K] for a non-negative i32 constant K — by ToInt32 semantics this holds for any e (NaN, fractional, negative, non-numeric);
  • e >>> k → ToUint32 result bounded by the shift amount;
  • non-negative a | b / a & b windows (ones-cover bound).

Width-tracked typed-array reads like S[i & 1023] and S[256 + ((x >>> 16) & 0xff)] now prove bounds against the tracked view length and take the existing unchecked inline load (lower_typed_array_load) instead of an out-of-line getter. These ops are integral by construction, so a fractional index (which would read a named property, not an element) cannot slip through. An IndexGet range rule (element-kind value range, gated on the same bounds proof plus the same alias/capture checks the unchecked load requires) feeds the same analysis. The i32 fast path classifies proven in-bounds loads from int-element typed arrays (I8/U8/U8Clamped/I16/U16/I32 — not U32, which doesn't round-trip through a signed i32 slot) as native i32 leaves, so bit-mixing chains stay in add/xor i32.

2. Const typed-array lookup tables behave like integers (collectors/)

  • const S = new Int32Array(<literal len>) bindings that never escape element-access receiver position are recorded with their length; const a = S[x & 0xff]-style Lets (window statically inside [0, len)) seed integer_locals, with a matching exemption in the provenance judge. In-bounds int-TA loads are integers by construction; out-of-bounds (→ undefined) is excluded by the same window proof.
  • The pointer analysis knows a numerically-keyed read of a non-BigInt typed array never yields a pointer — killing the per-iteration GC shadow-slot spill calls the Blowfish round function was paying for its const a/b/c/d locals.

Together these turn x = (((a + b) ^ c) + d) | 0 from 4 dynamic-dispatch calls + ~13 shadow-slot calls per iteration into a straight-line i32 chain.

3. Read-only DENSE tier for the #6011 packed-f64 range loop (stmt/loops.rs, runtime)

The versioned-loop matcher now also accepts multi-statement read-only bodies whose plain-array reads carry static masked windows, guarded once at loop entry:

  • New runtime guards js_typed_feedback_packed_f64_range_loop_guard_dense{,_i32}: same plain-array/window validation as the simple EMA (Exponential Moving Average) loop over a 100k-element array runs **7x slower** in the compiled Perry binary (2300ms) compared to tsx (328ms) or node (343ms). #6011 range guard, but the window must be hole-free — the fast loop's loads then carry no hole check and no side exit, which is what makes multi-statement bodies safe to version (a mid-iteration side exit could double-apply earlier statement effects on re-execution; instead, an iteration runs entirely in one copy).
  • Two fast tiers: _dense_i32 additionally proves every window value is an i32-representable integer, so loads materialize with a bare exact fptosi and LLVM keeps (s + S[i & k]) | 0 loops fully in integer registers (and vectorizes them); _dense keeps raw-double loads for float lookup tables. Either guard failing falls through (i32 → f64 → slow).
  • Read eligibility no longer consults the array-kind / materialization-hazard facts: mark_unknown_call_escape blanket-hazards every function-local tracked array whenever the function contains any call (even a console.log after the loop), which kept every locally built lookup table (const S: number[] = new Array(1024) + fill loop — the S-box shape) off the fast path forever. The entry guard re-validates the actual runtime array — plain-array shape, raw-f64 packedness, frozen/descriptor/prototype-pollution state, and the whole index window — and the matched body admits no store/call/closure/await, so a wrong static hint costs one failed guard → slow loop, never correctness.

Measured results

Apple M1-class, perry-dev profile, PERRY_NO_AUTO_OPTIMIZE=1, outputs byte-identical to node --experimental-strip-types in every run:

benchmark (20M reads / 5M rounds) node perry before perry after
i32 arithmetic only 30 ms 26 ms 26 ms (unchanged)
plain Array read S[i & 1023] 21 ms 200 ms 4 ms
Int32Array read S[i & 1023] 16 ms 235 ms 10 ms
Blowfish-F mimic (4 S-box reads/round) 18 ms 558 ms 15 ms

Validation

  • Gap suite (./scripts/run_gap_tests.sh, release build): gate OK — 385 pass / 6 output mismatches / 1 crash, all 7 already triaged in known_failures.json (zlib params, defineProperty-on-class-prototype, iterator helpers, perf_hooks, stream tee tick parity, v8 module, SIGINT trace — none array/loop related), 98.2% parity, no new untriaged failures.
  • Edge-case suite (20 focused tests: OOB windows, holes, fractional/huge/NaN elements, string elements behind as any, frozen arrays, shrunk length, mixed counter+masked indices, all int TA kinds, 256 | (i & 255) windows, negative-operand masks, escape shapes, TA OOB/negative indices): byte-identical between this branch and origin/main. The four divergences from node it surfaced (string-literal coercion into a declared number[], string store through as any coercing to NaN, TA out-of-bounds undefined propagating through numeric adds as undefined/NaN) all reproduce identically on unpatched origin/main — pre-existing, not introduced here.
  • Unit/integration tests: perry-runtime typed_feedback (49) + array (66), perry-codegen lib (209), native_proof_regressions (242), native_proof_buffer_views (29/30), shadow_slot_hygiene (9) — the two failures observed (native_owned_uint8array_get_fallback_uses_uint8array_helper, typed_feedback_trace_dump_runs_before_entry_return, plus the i64_spec_ternary_recursion fixture that doesn't compile) fail identically on unpatched origin/main.
  • 52-test smoke over test_gap_*{array,loop,index,sort,typed}* with the dev build: no regressions vs baseline.

Not covered (follow-up)

Real bcryptjs compareSync is still slow (~3.9 s vs node 72 ms, unchanged from baseline): its _encipher receives the S/P boxes as untyped params (built via S_ORIG.slice() in plain JS, so no declared types for the matcher), uses a while loop, and increments the round index inside an index expression (P[++i]). Closing that needs typed-feedback-driven versioning for untyped parameters plus while-loop and impure-index support in the dense matcher — worth its own issue/PR.

Summary by CodeRabbit

  • Performance Improvements

    • Faster numeric range-based loops using dense, hole-free access windows.
    • Improved lowering for typed-array element reads, including provable i32-friendly paths.
    • Added masked-window lowering to reduce extra bounds and array-shape checks for eligible accesses.
  • Reliability

    • Strengthened typed-array bounds/value reasoning for more precise integer and range inference.
    • Enhanced dense packed-f64 loop guards with stronger validations for hole-free and i32-constrained windows.

…ked-index loops

Array element reads in tight loops compiled to one runtime guard call per
access (js_typed_feedback_numeric_array_index_get_guard and the typed-array
getters), making integer/array-heavy JS 10-15x slower than V8 even though
the element load itself was already inline. bcryptjs's Blowfish S-boxes are
exactly this shape, turning a login's compareSync into seconds of overhead.

Three complementary fixes:

1. Static index windows for the buffer bounds prover (expr/range_facts.rs):
   int_range_expr now understands 'e & K' (always [0, K] for a non-negative
   i32 constant mask, by ToInt32 semantics), 'e >>> k' (ToUint32 result
   bounded by the shift), and non-negative 'a | b' windows. Width-tracked
   typed-array reads like S[i & 1023] / S[256 + ((x >>> 16) & 0xff)] now
   prove bounds against the tracked view length and take the existing
   unchecked inline load instead of an out-of-line getter call. An IndexGet
   range rule (elem-kind value range, gated on the same bounds proof) feeds
   the same machinery. The i32 fast path classifies proven in-bounds loads
   from int-element typed arrays as native i32 leaves, so bit-mixing chains
   stay in add/xor i32 instead of round-tripping f64 through the branchless
   ToInt32 tower.

2. Collector support so const typed-array lookup tables behave like ints
   (collectors/): const S = new Int32Array(<literal>) bindings that never
   escape element-access receiver position are recorded with their length;
   'const a = S[x & 0xff]'-style Lets seed integer_locals (window proven
   inside [0, length)), and the pointer analysis knows numerically-indexed
   typed-array reads never yield pointers - killing the per-iteration GC
   shadow-slot spills and dynamic string_or_number_add dispatch the Blowfish
   round function was paying for.

3. Read-only DENSE tier for the PerryTS#6011 packed-f64 range loop (stmt/loops.rs,
   typed_feedback.rs): the versioned-loop matcher now also accepts
   multi-statement read-only bodies whose array reads carry static masked
   windows, guarded once at loop entry by a dense window guard (window must
   be hole-free - loads then need no hole check and no side exit, which is
   what makes multi-statement bodies safe to version). Two guard tiers:
   _dense_i32 additionally proves every window value is an i32 integer so
   loads materialize as a bare exact fptosi and LLVM keeps (s + S[i & k])|0
   loops fully in integer registers; _dense keeps raw-double loads for float
   lookup tables. Read eligibility no longer consults the array-kind /
   materialization-hazard facts: mark_unknown_call_escape blanket-hazards
   every function-local array whenever the function contains any call, which
   kept every locally built lookup table off the fast path forever; the
   entry guard re-validates the actual runtime array, so a wrong static hint
   costs one failed guard, never correctness.

Benchmarks (Apple M1-class, 20M reads / 5M Blowfish-F rounds, byte-identical
outputs vs node --experimental-strip-types):

                       node    perry before   perry after
  arith-only (20M)     30 ms       26 ms          26 ms
  plain-Array read     21 ms      200 ms           4 ms
  Int32Array read      16 ms      235 ms          10 ms
  Blowfish-F mimic     18 ms      558 ms          15 ms

An edge-case suite (OOB windows, holes, fractional/huge/NaN elements,
frozen arrays, string elements, shrunk length, mixed counter+masked
indices, all int TA kinds, escape shapes) is byte-identical between this
branch and origin/main; the four pre-existing divergences from node it
found are unrelated to this change and reproduce on main.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d0f6c6e1-ee53-4a3c-bf7e-5c2cd0583adb

📥 Commits

Reviewing files that changed from the base of the PR and between af2e705 and cf0cad8.

📒 Files selected for processing (1)
  • crates/perry-codegen/src/expr/index_get.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-codegen/src/expr/index_get.rs

📝 Walkthrough

Walkthrough

Changes

The change adds dense-window and dense-i32 guards for packed-f64 range loops, expands typed-array integer and range analysis, records masked-window facts, and lowers proven indexed loads directly. Compilation contexts now initialize the new fact storage.

Dense-window packed-f64 range loops

Layer / File(s) Summary
Typed-array and integer proofs
crates/perry-codegen/src/collectors/*, crates/perry-codegen/src/expr/range_facts.rs, crates/perry-codegen/src/expr/i32_fast_path.rs
Typed-array reads receive more precise pointer, integer, range, and i32 classification.
Window facts and runtime guards
crates/perry-codegen/src/expr/mod.rs, crates/perry-runtime/src/array/*, crates/perry-runtime/src/typed_feedback.rs, crates/perry-codegen/src/runtime_decls/objects.rs
Masked-window facts and dense runtime guards validate hole-free numeric windows, including an i32-specific tier.
Dense range-loop matching and guard emission
crates/perry-codegen/src/stmt/loops.rs
Range-loop matching records counter-relative and static windows, supports read-only DENSE loops, and emits two-tier guarded fast paths.
Masked-window indexed-load lowering
crates/perry-codegen/src/expr/index_get.rs, crates/perry-codegen/src/expr/masked_window.rs
Indexed loads use validated masked windows for numeric and i32 lowering before older fallback paths.
Compilation-context initialization
crates/perry-codegen/src/codegen/{closure,entry,function,method}.rs
All relevant FnCtx constructors initialize masked-window facts with empty vectors.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LoopMatcher
  participant Codegen
  participant TypedFeedback
  participant ArrayHeader
  participant FastLoop
  LoopMatcher->>Codegen: collect dense access windows
  Codegen->>TypedFeedback: emit dense or dense-i32 guard
  TypedFeedback->>ArrayHeader: validate numeric window
  ArrayHeader-->>TypedFeedback: return validation result
  TypedFeedback-->>Codegen: provide guard outcome
  Codegen->>FastLoop: lower masked-window indexed loads
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and accurately summarizes the main perf change in hot masked-index loop array reads.
Description check ✅ Passed The description is detailed and covers the changes, validation, and results; a few template sections like related issue and checklist are omitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…window.rs

The masked-window fact consult + the two load-emission tiers pushed
expr/index_get.rs past the 2000-line lint cap (check_file_size.sh); move
them to a topical sibling module. Pure move plus factoring the shared
raw in-window load emission; no behavior change (benchmarks and the
edge-case suite are byte-identical before/after).

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
crates/perry-codegen/src/expr/index_get.rs (1)

158-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate raw-load sequence between the f64 and i32 masked-window loaders.

lower_masked_window_index_get (170-180) and lower_masked_window_index_get_i32 (250-260) emit the identical bitcast→mask→zext→shl 3→+8→+handle→inttoptr→load-double sequence. Extracting a shared helper (returning the raw f64 SSA value) would avoid the two copies diverging silently.

♻️ Suggested extraction
+fn masked_window_raw_f64_load(ctx: &mut FnCtx<'_>, arr_box: &str, idx_i32: &str) -> String {
+    let blk = ctx.block();
+    let arr_bits = blk.bitcast_double_to_i64(arr_box);
+    let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64);
+    let idx_i64 = blk.zext(I32, idx_i32, I64);
+    let byte_offset = blk.shl(I64, &idx_i64, "3");
+    let with_header = blk.add(I64, &byte_offset, "8");
+    let element_addr = blk.add(I64, &arr_handle, &with_header);
+    let element_ptr = blk.inttoptr(I64, &element_addr);
+    blk.load(DOUBLE, &element_ptr)
+}

Then both lower_masked_window_index_get and lower_masked_window_index_get_i32 call masked_window_raw_f64_load(ctx, arr_box, idx_i32) instead of repeating the sequence inline.

Also applies to: 232-299

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/index_get.rs` around lines 158 - 216, Extract
the duplicated raw masked-window load sequence into a shared helper named
masked_window_raw_f64_load, returning the raw f64 SSA value. Update both
lower_masked_window_index_get and lower_masked_window_index_get_i32 to call this
helper with ctx, arr_box, and idx_i32, while preserving their existing value
recording and metadata behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-codegen/src/expr/index_get.rs`:
- Around line 158-216: Extract the duplicated raw masked-window load sequence
into a shared helper named masked_window_raw_f64_load, returning the raw f64 SSA
value. Update both lower_masked_window_index_get and
lower_masked_window_index_get_i32 to call this helper with ctx, arr_box, and
idx_i32, while preserving their existing value recording and metadata behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 41cc2a8b-2778-42b3-ac2b-4d98b112a172

📥 Commits

Reviewing files that changed from the base of the PR and between 21c94f9 and cb38492.

📒 Files selected for processing (16)
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/collectors/integer_locals.rs
  • crates/perry-codegen/src/collectors/mod.rs
  • crates/perry-codegen/src/collectors/pointer_locals.rs
  • crates/perry-codegen/src/expr/i32_fast_path.rs
  • crates/perry-codegen/src/expr/index_get.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/range_facts.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-codegen/src/stmt/loops.rs
  • crates/perry-runtime/src/array/header.rs
  • crates/perry-runtime/src/array/mod.rs
  • crates/perry-runtime/src/typed_feedback.rs

…plit module

The module split in the previous commit missed the second consult site
(the generic lower IndexGet arm) — it still called the moved functions
unqualified and the crate did not compile. Verified with a from-scratch
build this time; benchmarks and the edge suite are unchanged.
@proggeramlug
proggeramlug merged commit 20854f3 into PerryTS:main Jul 22, 2026
27 checks passed
@proggeramlug
proggeramlug deleted the perf/array-index-guard-hoist branch July 22, 2026 09:29
proggeramlug pushed a commit that referenced this pull request Jul 22, 2026
…ray params

Follow-up to #6750: the dense masked-index range-loop versioning only fired
for bindings whose STATIC type proved a number array, so an array arriving as
an untyped (any) function parameter — the bcryptjs Blowfish S-box shape —
kept paying one guard call per access (~40x slower than Node on
S[i & 1023] loops).

Two changes:

1. Matcher: read-only DENSE accesses also admit bindings with no usable
   static type (any/unknown/untracked). Safe because the entry guards
   re-validate the actual runtime value; a wrong hint costs one failed
   guard -> slow loop. Untyped plain-Array params now version through the
   existing dense_i32/dense plain-array tiers unchanged.

2. New typed-array tiers ahead of the plain tiers, emitted only when at
   least one accessed binding is untyped and every access carries a static
   window: one O(1) probe per array
   (js_typed_feedback_masked_window_ta_kind: registry lookup + window/length
   compare, detached views rejected via length 0) classifies the receiver as
   Int32Array / Uint32Array / Float64Array; all arrays agreeing on a kind
   branches into a fast copy whose loads go through the preheader-hoisted
   element-0 data pointer (js_typed_array_masked_window_data_ptr) with the
   width-correct inline load: i32 loads stay native i32 in bit-mixing chains
   (values_i32), u32 materializes unsigned (uitofp, never i32-classified),
   f64 loads directly. The hoisted pointer is stable because the fast copy
   is call-free (no allocation -> no GC) and view backings are
   thread-lifetime allocations.
proggeramlug added a commit that referenced this pull request Jul 26, 2026
…ops (#6850)

* perf(codegen): lower Math.imul and typed-array-param reads to native ops

Two integer-math JS primitives were compiled as runtime calls and cascaded a
whole hot integer loop into slow f64 ToInt32 towers.

- Math.imul(a,b) -> native `mul i32` when both operands are provable i32 (exact:
  the low 32 bits of the product are identical for signed/unsigned operands).
  Non-finite / fractional / >2^32 operands keep the js_math_imul helper so JS
  ToUint32/ToInt32 semantics are preserved. Because `Math.imul` is defined as
  ToInt32(ToUint32*ToUint32 mod 2^32), an integer-literal operand is exact under
  low-32 truncation even above i32::MAX, so the imul operand check now accepts
  32-bit-representable literals -- fixing the accumulator case
  `a = Math.imul(a, 0x9e3779b1)` (golden-ratio constant 2654435761 > i32::MAX).
  This relaxation is confined to Math.imul operands: a plain `x * BIGLIT | 0`
  evaluates its product in f64 (precision loss above 2^53), so it must stay
  ToInt32(f64_product), never an exact `mul i32`.

- Typed-array element read through a parameter (S[i] where S: Int32Array etc. is
  a function parameter, in an i32/ToInt32 context) -> checked inline native
  load: a runtime guard (pointer + inline-storage PERRY_TA_VIEW_GUARD +
  kind-cache) and a header-length bounds check gate a bare width-correct load;
  an in-kind out-of-bounds read yields 0 (== ToInt32(undefined)); every rejected
  shape (view/detached/resizable backing, wrong runtime kind) defers to the new
  js_typed_array_read_int32 fallback. Extends the #6750 typed-array-local bare
  load to parameters, whose length/storage are unknown at compile time.
  Plain-value parameter reads still observe undefined out of bounds.

On the 40M-iteration Int32Array-parameter bit-mixer both runtime-call families
(js_math_imul x3, js_typed_array_get x3) reach zero and the read/multiply chain
stays in native i32.

Adds gap tests test_gap_math_imul_native.ts and test_gap_typedarray_param_read.ts
(byte-identical to node --experimental-strip-types).

* docs(changelog): key fragment to PR #6850

* fix(codegen,runtime): address CodeRabbit review on the native typed-array-param read

Two Major findings on #6850:

1. Correctness — the checked typed-array-param i32 fast path gated on the
   receiver kind but NOT the index: `S[3.9]` in an i32/ToInt32 context lowered
   the index through `fptosi` (=3) and read element 3, but JS reads a fractional
   typed-array index as `undefined` (-> 0 in that consumer). Gate the arm on
   `numeric_index_has_integer_array_index_proof` (made pub(crate)) — the same
   integer-index proof the sibling typed-array read paths in index_get.rs use.
   Integer literals, `i & mask`, and i32 loop counters keep the native path.

2. Memory safety — `js_typed_array_read_int32` (cold fallback) called
   `js_typed_array_get`, which reads `(*ta).length` (a TypedArrayHeader field)
   before classifying the pointer. That fallback is reached on a wrong-kind /
   kind-cache miss, which includes a receiver that is NOT a typed array (a
   `f(S: Int32Array)` called with an arbitrary value — TS types are erased), so
   a non-typed-array pointer type-confused the first GC-header read. Validate the
   raw pointer with `lookup_typed_array_kind` (the same gate
   `strict_typed_array_from_raw` uses; it covers native/inline views) before any
   header access; a non-array receiver returns 0 (= ToInt32(undefined)).

Gap test extended with the fractional-index-in-i32-context regression cases,
byte-identical to Node.

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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.

1 participant