To make a long story shorter, I've been aggressively profiling the generative tests I rely on on a daily basis. In CI, they run on memory-constrained machines which have occasionally ended up in OOM states that cause our test executor to kill the nodes running some generative tests. In many cases, we've been able to adjust sizing to get under the line and resume normally. I've been running into that case more frequently, and have been looking for potential causes/levers to adjust in the source code.
The below findings are from my relatively powerful Macbook:
Operating System: MacOS (Tahoe - AArch64)
Clojure version: Verified on 1.12.1, Compared against 1.11.4
JDK vendor and version: OpenJDK 21.0.11
Using clj-async-profiler and an allocation/timing macro from Clojure Goes Fast, I benchmarked clojure.test.check.generators against some pretty common generators (Assume I'm using gen as an alias for that namespace):
gen/string
gen/large-integer
(gen/map gen/keyword gen/string)
While benchmarking, ReentrantLock (and ReentrantLock$NonfairSync), accounted for ~1/3rd of all allocations. Since Clojure 1.12, each LazySeq also allocates a ReentrantLock, so I reran the benchmarking after downgrading to Clojure 1.11.4 Obviously, all of those object references were no longer in the profiler results, but the amount of bytes allocated did consistently shrink (~10-15%, depending on the generator).
Most of the allocation appears to be happening while constructing the children segments of nodes in the rose tree. To my knowledge, we only ever lean on the full tree when shrinking results, and wouldn't read them on a generate-only pass. To test that, I wrapped the children arguments to a few of the make-rose calls in the call tree for string generation, and re-instrumented on v.12.1
This approximated the same reduction in memory use as downgrading to 1.11.4, and wiped out almost all of the ReentrantLock allocation events. I've been able to verify the fix does not break any tests, and that shrinking is functional as well.
For posterity / reproduction, the specific call sites I wrapped the children in were:
clojure.test.check.rose-tree
join
fmap
shrink
shrink-vector*
clojure.test.check.generators
Happy to share the profiler output / git patch if anyone is interested!