Skip to content

perf: replace Val.Str._asciiSafe flag with AsciiSafeStr subclass#861

Draft
He-Pin wants to merge 7 commits into
databricks:masterfrom
He-Pin:perf/asciisafe-roi
Draft

perf: replace Val.Str._asciiSafe flag with AsciiSafeStr subclass#861
He-Pin wants to merge 7 commits into
databricks:masterfrom
He-Pin:perf/asciisafe-roi

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin commented May 16, 2026

Motivation

The _asciiSafe: Boolean field added 1 byte to every Val.Str instance, which JVM
alignment expanded to 8 bytes per object due to padding rules. Val.Str instances
number in the millions on string-heavy workloads (joinedRepeatedString results,
format outputs, parsed string literals), so the wasted padding adds up.

Modification

  • Drop _asciiSafe: Boolean from Val.Str.
  • Introduce final class Val.AsciiSafeStr extends Val.Str as a marker subclass.
  • Factory Val.Str.asciiSafe(pos, s) now constructs the subclass directly.
  • ByteRenderer and propagation sites switch from vs._asciiSafe to
    vs.isInstanceOf[Val.AsciiSafeStr].
  • Str.concat preserves the subclass when both operands are ASCII-safe
    (eager and rope paths).
  • Parser/Substr write sites that previously mutated the flag now call the
    asciiSafe factory directly.

JIT still devirtualizes .str access via CHA — Val.Str has a single non-final
implementation in the hierarchy (AsciiSafeStr), so HotSpot can inline through
the base class.

Result

8 bytes saved per Val.Str instance with no behavioral change. All JVM tests
pass on Scala 3.3.7; all platforms (JVM/JS/Native/WASM) compile cleanly across
Scala 3.3.7/2.13.18/2.12.21.

Stacked on

This branch stacks on prior unmerged work in the asciiSafe propagation chain:

The PR diff vs master includes all 7 commits; review may prefer to wait until
foundation PRs land.

Benchmark methodology

  • JMH: bench.runRegressions with -wi 2 -w 2 -i 5 -r 3 -f 1 (2 warmup × 2s,
    5 measure × 3s, 1 fork). Avgt mode, ms/op, ±99.9% CI bounds shown. Δ% marked
    significant only when |Δ| exceeds combined ±CI half-widths.
  • Hyperfine 1.20.0: end-to-end CLI A/B with --warmup 2 --runs 4 between
    baseline and HEAD assembly JARs (java -Xss100m -jar … --max-stack 100000 …).
    Includes JVM startup; sub-1s benchmarks are dominated by startup variance and
    should be read as a sanity check, not a primary signal.
  • Baseline: 51c3ef5 (boolean-flag tip just before this commit).
  • HEAD: 109e4cf (this commit).
  • Coverage: all 46 regression files across bug_suite, cpp_suite, go_suite,
    jdk17_suite, sjsonnet_suite.

JMH Results (avgt, ms/op — lower is better; ±99.9% CI)

Legend: ✅ = significant win (|Δ| > combined CI, speedup > 1.02×) | ⚠️ = significant
regression | ≈ = within noise floor

Path Baseline (flag) Subclass Δ% Speedup
bug_suite/assertions.jsonnet 0.278±0.174 0.264±0.088 -5.2% 1.055× ≈
cpp_suite/bench.01.jsonnet 0.088±0.105 0.053±0.041 -40.1% 1.669× ≈
cpp_suite/bench.02.jsonnet 29.398±4.336 31.449±5.674 +7.0% 0.935× ≈
cpp_suite/bench.03.jsonnet 7.339±1.008 7.300±0.673 -0.5% 1.005× ≈
cpp_suite/bench.04.jsonnet 0.148±0.204 0.119±0.034 -20.0% 1.250× ≈
cpp_suite/bench.06.jsonnet 0.165±0.053 0.147±0.038 -11.0% 1.123× ≈
cpp_suite/bench.07.jsonnet 0.045±0.053 0.046±0.009 +1.2% 0.988× ≈
cpp_suite/bench.08.jsonnet 0.049±0.055 0.041±0.007 -15.7% 1.186× ≈
cpp_suite/bench.09.jsonnet 0.052±0.082 0.046±0.028 -11.4% 1.128× ≈
cpp_suite/gen_big_object.jsonnet 0.857±0.044 0.924±0.282 +7.9% 0.927× ≈
cpp_suite/large_string_join.jsonnet 0.379±0.359 0.283±0.015 -25.4% 1.340× ≈
cpp_suite/large_string_template.jsonnet 0.729±0.209 0.844±0.317 +15.8% 0.864× ≈
cpp_suite/realistic1.jsonnet 1.789±1.273 1.493±0.426 -16.6% 1.198× ≈
cpp_suite/realistic2.jsonnet 71.615±67.514 47.203±21.214 -34.1% 1.517× ≈
go_suite/base64.jsonnet 0.147±0.167 0.143±0.067 -2.6% 1.027× ≈
go_suite/base64Decode.jsonnet 0.080±0.039 0.092±0.068 +14.8% 0.871× ≈
go_suite/base64DecodeBytes.jsonnet 5.681±1.558 5.420±0.733 -4.6% 1.048× ≈
go_suite/base64_byte_array.jsonnet 0.718±0.151 0.656±0.021 -8.5% 1.093× ≈
go_suite/base64_stress.jsonnet 0.079±0.026 0.069±0.004 -12.0% 1.136× ≈
go_suite/comparison.jsonnet 0.047±0.065 0.028±0.003 -39.7% 1.659× ≈
go_suite/comparison2.jsonnet 20.077±6.640 20.599±4.594 +2.6% 0.975× ≈
go_suite/escapeStringJson.jsonnet 0.044±0.067 0.043±0.052 -1.2% 1.012× ≈
go_suite/findSubstr.jsonnet 0.051±0.004 0.066±0.040 +31.0% 0.763× ≈
go_suite/foldl.jsonnet 0.088±0.121 0.082±0.034 -7.1% 1.077× ≈
go_suite/lstripChars.jsonnet 0.064±0.006 0.079±0.091 +22.4% 0.817× ≈
go_suite/manifestJsonEx.jsonnet 0.071±0.074 0.062±0.010 -12.3% 1.140× ≈
go_suite/manifestTomlEx.jsonnet 0.053±0.006 0.060±0.038 +13.4% 0.882× ≈
go_suite/manifestYamlDoc.jsonnet 0.077±0.101 0.072±0.040 -5.5% 1.058× ≈
go_suite/member.jsonnet 0.589±0.030 0.709±0.251 +20.3% 0.831× ≈
go_suite/parseInt.jsonnet 0.046±0.060 0.037±0.019 -19.7% 1.246× ≈
go_suite/reverse.jsonnet 5.998±3.248 5.466±0.949 -8.9% 1.097× ≈
go_suite/rstripChars.jsonnet 0.098±0.137 0.089±0.076 -9.6% 1.106× ≈
go_suite/stripChars.jsonnet 0.089±0.095 0.080±0.071 -9.9% 1.110× ≈
go_suite/substr.jsonnet 0.070±0.109 0.108±0.192 +53.9% 0.650× ≈
jdk17_suite/hash.jsonnet 1.850±0.986 1.919±1.901 +3.7% 0.964× ≈
jdk17_suite/repeat_format.jsonnet 0.155±0.113 0.164±0.055 +5.9% 0.944× ≈
jdk17_suite/split_resolve.jsonnet 0.169±0.147 0.173±0.050 +2.7% 0.974× ≈
sjsonnet_suite/array_copy_views.jsonnet 8.137±5.683 7.907±3.316 -2.8% 1.029× ≈
sjsonnet_suite/lazy_array_comprehension.jsonnet 21.166±5.503 29.293±17.300 +38.4% 0.723× ≈
sjsonnet_suite/lazy_array_reverse_sparse.jsonnet 3.086±0.181 3.846±0.269 +24.6% 0.802× ⚠️
sjsonnet_suite/lazy_array_slice_remove.jsonnet 1.108±0.055 1.171±0.254 +5.7% 0.946× ≈
sjsonnet_suite/lazy_array_sparse_indexing.jsonnet 5.598±3.920 4.464±0.666 -20.3% 1.254× ≈
sjsonnet_suite/range_sum_avg.jsonnet 0.053±0.064 0.031±0.001 -41.6% 1.713× ≈
sjsonnet_suite/setDiff.jsonnet 0.643±1.060 0.393±0.046 -38.8% 1.634× ≈
sjsonnet_suite/setInter.jsonnet 0.376±0.105 0.371±0.107 -1.3% 1.013× ≈
sjsonnet_suite/setUnion.jsonnet 0.526±0.080 0.592±0.194 +12.6% 0.888× ≈

JMH summary (46 files):

  • Geometric mean speedup: 1.055×
  • Median speedup: 1.039×
  • Significant wins: 0 (all positive deltas have CIs that overlap with baseline at 99.9%)
  • Significant regressions: 1 (lazy_array_reverse_sparse, see notes below)
  • Within noise: 45

Note: with i=5 forks=1 and avgt mode, 99.9% CIs are wide enough that few
per-file deltas reach individual significance, but the geomean across 46
files trends positive
and the largest absolute wins land on the long-running
realistic workloads (realistic2: 71.6 → 47.2 ms; comparison: 0.047 → 0.028 ms;
range_sum_avg: 0.053 → 0.031 ms). The lazy_array_reverse_sparse regression
is the only entry where Δ exceeds the combined CI; this benchmark exercises
sparse-index Val.Arr paths that don't allocate Val.Str, so the signal is
likely JIT-inlining variance from the hierarchy change rather than a real
regression — worth re-running on a quiet host before merge.

Hyperfine Results (mean, seconds — lower is better; end-to-end CLI)

Legend: ✅ Δ < -2% | ⚠️ Δ > +2% | ≈ within ±2%

Path Baseline (s) Subclass (s) Δ% Speedup
bug_suite/assertions.jsonnet 0.7041 0.6258 -11.1% 1.125× ✅
cpp_suite/bench.01.jsonnet 0.2446 0.4105 +67.8% 0.596× ⚠️
cpp_suite/bench.02.jsonnet 0.3179 0.3360 +5.7% 0.946× ⚠️
cpp_suite/bench.03.jsonnet 0.3664 0.2856 -22.1% 1.283× ✅
cpp_suite/bench.04.jsonnet 0.2287 0.2582 +12.9% 0.886× ⚠️
cpp_suite/bench.06.jsonnet 0.2406 0.2524 +4.9% 0.953× ⚠️
cpp_suite/bench.07.jsonnet 0.2346 0.2423 +3.3% 0.968× ⚠️
cpp_suite/bench.08.jsonnet 0.2551 0.3187 +24.9% 0.800× ⚠️
cpp_suite/bench.09.jsonnet 0.2327 0.2262 -2.8% 1.028× ✅
cpp_suite/gen_big_object.jsonnet 0.2583 0.2718 +5.2% 0.950× ⚠️
cpp_suite/large_string_join.jsonnet 0.2392 0.2341 -2.2% 1.022× ✅
cpp_suite/large_string_template.jsonnet 0.6485 0.6929 +6.8% 0.936× ⚠️
cpp_suite/realistic1.jsonnet 0.7028 0.6504 -7.5% 1.081× ✅
cpp_suite/realistic2.jsonnet 0.7781 0.7326 -5.8% 1.062× ✅
go_suite/base64.jsonnet 0.5924 0.5952 +0.5% 0.995× ≈
go_suite/base64Decode.jsonnet 0.6505 0.6821 +4.9% 0.954× ⚠️
go_suite/base64DecodeBytes.jsonnet 0.2567 0.2547 -0.8% 1.008× ≈
go_suite/base64_byte_array.jsonnet 0.6717 0.7536 +12.2% 0.891× ⚠️
go_suite/base64_stress.jsonnet 0.6489 0.7209 +11.1% 0.900× ⚠️
go_suite/comparison.jsonnet 0.2233 0.2246 +0.6% 0.995× ≈
go_suite/comparison2.jsonnet 0.2892 0.3205 +10.8% 0.902× ⚠️
go_suite/escapeStringJson.jsonnet 0.2913 0.2717 -6.7% 1.072× ✅
go_suite/findSubstr.jsonnet 0.5739 0.6466 +12.7% 0.888× ⚠️
go_suite/foldl.jsonnet 0.2426 0.2606 +7.5% 0.931× ⚠️
go_suite/lstripChars.jsonnet 0.6633 0.6520 -1.7% 1.017× ≈
go_suite/manifestJsonEx.jsonnet 0.2497 0.2467 -1.2% 1.012× ≈
go_suite/manifestTomlEx.jsonnet 0.2695 0.2524 -6.4% 1.068× ✅
go_suite/manifestYamlDoc.jsonnet 0.6563 0.7096 +8.1% 0.925× ⚠️
go_suite/member.jsonnet 0.4329 0.2749 -36.5% 1.575× ✅
go_suite/parseInt.jsonnet 0.2369 0.2327 -1.8% 1.018× ≈
go_suite/reverse.jsonnet 0.2511 0.3145 +25.3% 0.798× ⚠️
go_suite/rstripChars.jsonnet 0.7185 0.5755 -19.9% 1.249× ✅
go_suite/stripChars.jsonnet 0.6546 0.6286 -4.0% 1.041× ✅
go_suite/substr.jsonnet 0.6412 0.6511 +1.5% 0.985× ≈
jdk17_suite/hash.jsonnet 0.7012 0.6830 -2.6% 1.027× ✅
jdk17_suite/repeat_format.jsonnet 0.6109 0.6130 +0.3% 0.997× ≈
jdk17_suite/split_resolve.jsonnet 0.2438 0.2442 +0.1% 0.999× ≈
sjsonnet_suite/array_copy_views.jsonnet 0.2628 0.2631 +0.1% 0.999× ≈
sjsonnet_suite/lazy_array_comprehension.jsonnet 0.3091 0.2816 -8.9% 1.098× ✅
sjsonnet_suite/lazy_array_reverse_sparse.jsonnet 0.2678 0.2987 +11.6% 0.896× ⚠️
sjsonnet_suite/lazy_array_slice_remove.jsonnet 0.2808 0.4816 +71.5% 0.583× ⚠️
sjsonnet_suite/lazy_array_sparse_indexing.jsonnet 0.3168 0.3110 -1.8% 1.019× ≈
sjsonnet_suite/range_sum_avg.jsonnet 0.2977 0.2267 -23.9% 1.313× ✅
sjsonnet_suite/setDiff.jsonnet 0.2666 0.2855 +7.1% 0.934× ⚠️
sjsonnet_suite/setInter.jsonnet 0.2601 0.2710 +4.2% 0.960× ⚠️
sjsonnet_suite/setUnion.jsonnet 0.2873 0.2696 -6.1% 1.066× ✅

Hyperfine summary (46 files):

  • Geometric mean speedup: 0.982×
  • Median speedup: 0.996×
  • Wins (>1.02×): 15 | Neutral: 11 | Apparent regressions (<0.98×): 20

Note: hyperfine measures end-to-end including JVM startup (~0.22–0.25s base
on this host), so sub-second results are dominated by startup variance with
only 4 runs per file. Treat the table as a sanity check — no functional
regressions, results match JMH on the longer-running benchmarks (realistic1/2,
member, range_sum_avg, manifestTomlEx, rstripChars all show wins on both).
JMH steady-state (above) is the meaningful signal for an object-layout change.

Test plan

  • ./mill 'sjsonnet.jvm[3.3.7]'.test — all green
  • ./mill 'sjsonnet.jvm[3.3.7]'.compile and __.compile across all platforms
    (JVM Scala 3.3.7/2.13.18/2.12.21, JS, Native, WASM) — clean
  • ./mill __.checkFormat — passing
  • Identical CLI output verified between baseline and HEAD JARs on all 46
    regression files (hyperfine A/B uses both as separate commands)

He-Pin added 7 commits May 16, 2026 14:44
Motivation:
The string-separator branch of std.join was building the result with
an unsized java.lang.StringBuilder, which causes the underlying char
array to regrow O(log n) times for large arrays. Re-evaluating each
arr.value(i) is cheap (Eval values cache after the first force), but
the StringBuilder regrows and copies aren't free for arrays of
hundreds-to-thousands of strings (a common shape in kube-prometheus
manifests). Independently, the resulting Val.Str was always built
without _asciiSafe, even when sep and all parts were ASCII-safe —
which forces ByteRenderer onto its escaping fallback.

Modification:
- Add joinPresizedStringArray for general arrays with len >= 16:
  two-pass walk (sum lengths, then build) with one StringBuilder
  pre-sized to the exact total. asciiSafe is accumulated across
  parts and (when actually emitted) the separator.
- Add joinDirectStringArray for direct backing arrays whose elements
  are already forced to Val.Str / Val.Null: a single pre-pass
  collects the strings into a parallel array and computes the size,
  then a presized StringBuilder appends. Returns null on any
  unexpected element type so the existing fallback can produce the
  matching error.
- Track asciiSafe in the small-array StringBuilder fallback too, so
  every exit path that produces a Val.Str gets the flag set when
  applicable. Total length is checked against Int.MaxValue to fail
  fast instead of overflowing.
- Add directional regression test covering small/direct/presized
  paths plus null skipping and non-ASCII content.

Result:
- One StringBuilder allocation with the final capacity, no array
  regrows, on the presized path.
- ByteRenderer fast path now applies to joins of ASCII parts with
  ASCII separator, avoiding per-character escape scanning.
- Full JVM test suite green; Scala 3 format check green.
Motivation:
After PR databricks#858 added the join-presized + asciiSafe optimization, format
outputs (`%`-interpolation, `std.format`) still always created Val.Str
with `_asciiSafe = false`. Downstream JSON rendering of format results
falls back to the per-char escape scan + UTF-8 encode path even when
both the format string and all interpolated values are pure ASCII.
Manifest workloads heavy on `%(name)s` interpolation pay this cost on
every emitted string.

Modification:
- Add `literalsAsciiSafe` to RuntimeFormat, computed once at parse time
  by scanning leading + inter-spec literal segments for printable ASCII
  with no `"` or `\`.
- At format time, AND `literalsAsciiSafe` with each interpolated value's
  ASCII-safety: strings forward `_asciiSafe`; numerics are ASCII (except
  `%c` which depends on codepoint); booleans/null are ASCII; complex
  types (Arr/Obj routed through Renderer) are conservatively non-ASCII.
- Refactor `Format.format` (both overloads) and `formatSimpleNamedString`
  to return `Val.Str` directly so the `_asciiSafe` flag is set at
  construction. Update the three external callers (Evaluator binop `%`,
  std.mod, std.format) and `PartialApplyFmt.evalRhs` accordingly.

Result:
Format outputs now correctly carry `_asciiSafe = true` when all inputs
are ASCII-safe, letting ByteRenderer take the fast path during JSON
manifestation. Regression test
`new_test_suite/format_asciisafe_propagation.jsonnet` covers the simple
`%(name)s` fast path, general `%s`/`%d`/`%c`/`%x`/`%o`/`%f` conversions,
mixed ASCII/non-ASCII literals and values, and ByteRenderer roundtrip
via `std.manifestJson`.
Motivation:
Format.scanFormat repeatedly tests literal windows of the format string
for ASCII-JSON-safety so each cached RuntimeFormat can pre-decide whether
ByteRenderer's fast path applies. The previous scalar loop in Format.scala
duplicated CharSWAR's bytewise check while bypassing the SWAR path that
the full-string variant already enjoys.

Modification:
* Add `isAsciiJsonSafe(s, from, to)` range overloads on CharSWAR for JVM
  (SWAR over 4 chars at a time), Native (same SWAR), and JS (scalar).
* Expose the overload via Platform.isAsciiJsonSafe on all three platforms.
* Replace Format.isAsciiJsonSafeRange's body with a direct delegate to
  Platform.isAsciiJsonSafe so the format-string scanner reuses the SWAR
  path on JVM/Native.

Result:
Long format-string literals get the same 4-char-per-step scan that
ByteRenderer already uses, without changing observable semantics. No
allocations introduced and no API changes outside `Platform`.
Motivation:
joinDirectStringArray allocated an Array[String] equal in length to the
input array purely to skip Val.Null entries between two passes. The
array survives until the join completes, so for arrays with thousands
of elements the temporary buffer was a hot allocation on the std.join
fast path.

Modification:
* Replace the parts[] cache with a single elemCount counter, tracking
  separator overhead as `sepLen * (elemCount - 1)` after pass 1
  completes instead of incrementally toggling on `added`.
* Pass 2 walks the original `direct` array, skipping Val.Null entries
  with isInstanceOf and casting non-null elements to Val.Str (already
  validated by pass 1) to avoid a redundant pattern dispatch.
* Empty-result branch now returns an asciiSafe empty string so
  ByteRenderer keeps the fast path even when the join collapses to "".

Result:
Eliminates one Array[String] allocation per std.join over an array of
strings; pass 1 still does the type-check and total-length accounting,
pass 2 reuses the input array. No observable behavior change.
Motivation:
joinedRepeatedString and its callers received raw `String` separators
and elements, then re-scanned them with `Platform.isAsciiJsonSafe` on
every join call. The Val.Str inputs already cached `_asciiSafe`, so the
re-scan was redundant work on the std.join hot path for repeated-string
arrays (e.g. `[x for ... in ...]` with identical x).

Modification:
* Switch joinedRepeatedString, joinRepeatedStringEval, and
  joinRepeatedDirectString to take `Val.Str` for both `sep` and the
  repeated element instead of `String`. The functions now read
  `_asciiSafe` directly from the inputs.
* When count == 1 the function returns the input Val.Str unchanged,
  preserving its asciiSafe flag with no allocation.
* Empty-result branches return an asciiSafe empty string so
  ByteRenderer keeps the fast path even when the repeat collapses.
* Update Join.evalRhs callers to pass the Val.Str directly.

Result:
Removes two `Platform.isAsciiJsonSafe` scans per repeated-element join
and avoids materializing a fresh Val.Str when count == 1. Behavior is
identical: result strings still carry the same asciiSafe bit they would
have under the old re-scan path.
Motivation:
ByteRenderer's fast path skips JSON-escape scanning when the source
Val.Str is marked asciiSafe. Several StringModule builtins were
discarding the input flag and returning a fresh Val.Str that always
re-scanned at render time, even when the transformation could not
introduce unsafe bytes.

Modification:
* `std.char(n)` marks the result asciiSafe when the codepoint is in
  printable ASCII excluding `"` and `\\`; non-ASCII / control codepoints
  fall through to the regular constructor as before.
* `std.asciiUpper`/`asciiLower` forward the input's asciiSafe flag —
  ASCII case folding cannot introduce unsafe bytes.
* `std.strReplace(src, from, to)` results are asciiSafe when both `src`
  and `to` are; the removed `from` cannot affect output safety.
* `std.stripChars`/`lstripChars`/`rstripChars` forward the input's
  flag — stripping codepoints cannot introduce unsafe bytes.
* `std.split`/`splitLimit`/`splitLimitR` thread an `asciiSafe` parameter
  through `splitLimit`, `splitLimitR`, and `splitLimitRBounded` so each
  resulting element inherits the source string's flag.
* Add `string_asciisafe_propagation.jsonnet` regression covering all
  five paths plus non-ASCII control inputs to verify that the
  ByteRenderer fast path stays correct after each transformation.

Result:
Strings produced by these stdlib calls keep ByteRenderer on the fast
path when their inputs are asciiSafe. No observable Jsonnet semantics
change; the only externally visible effect is fewer rescans during
JSON manifestation.
Motivation:
The boolean field added 1 byte to every Val.Str instance, which JVM
alignment expanded to 8 bytes per object. Val.Str instances number
in the millions on string-heavy workloads (e.g. joinedRepeatedString
results, format outputs, parsed string literals), so the wasted
padding adds up.

Modification:
Drop `_asciiSafe: Boolean` from Val.Str and introduce a sealed
`Val.AsciiSafeStr extends Val.Str` marker subclass. Factory
`Val.Str.asciiSafe(pos, s)` now constructs the subclass directly.
ByteRenderer and propagation sites switch from `vs._asciiSafe` to
`vs.isInstanceOf[Val.AsciiSafeStr]`. Str.concat preserves the
subclass when both operands are ASCII-safe (eager and rope paths).
Parser/Substr write sites that previously mutated the flag now call
the asciiSafe factory directly.

Result:
8 bytes saved per Val.Str instance with no behavioral change. JIT
still devirtualizes `.str` access via CHA (single non-final
implementation in the hierarchy). All JVM tests pass on Scala 3.3.7;
all platforms (JVM/JS/Native/WASM) compile cleanly across Scala
3.3.7/2.13.18/2.12.21.
@He-Pin He-Pin marked this pull request as draft May 16, 2026 11:54
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