DO NOT MERGE: #34154: test(java25): runnable demos for carrier scheduling and compact object headers - #37117
DO NOT MERGE: #34154: test(java25): runnable demos for carrier scheduling and compact object headers#37117fabrizzio-dotCMS wants to merge 4 commits into
Conversation
…t headers Four main-class demos under com.dotcms.jdk, built for the Java 21-25 tech talk. They are the evidence half of the talk: each turns a claim about the JVM into something an audience can watch happen, using this codebase's own classes and its own incidents. - VirtualThreadCarrierTimelineDemo draws one row per carrier and one column per 100 ms tick, showing which virtual thread was mounted. mode=file comes out identical to a CPU hog while mode=socket frees both carriers at once, which is the shape of #37038. - VirtualThreadCarrierStarvationDemo contrasts a virtual-thread executor with a fixed platform pool: both start 2 of 10 tasks, but only the platform run lets an unrelated virtual thread proceed. - VirtualThreadYieldVsParkDemo separates yielding from parking on the same two carriers. - CompactObjectHeadersDemo relaunches itself in two child JVMs, one per setting of -XX:+/-UseCompactObjectHeaders, and measures the real ContentSearchHit and SiteSearchHit records with the thread allocation counter plus a heap-delta cross-check. No production code is touched and nothing new runs in CI beyond compilation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fabrizzio-dotCMS's task in 1m 59s —— View job Review: PR #37117 — runnable JDK demosRead all seven files. These are New Issues
Everything else checks out: the
|
Adds CustomCollectorDemo to the talk demos. Four sections, all runnable with `java <path>.java`: - the anatomy (supplier/accumulator/combiner/finisher) on toImmutableList, the collector everyone writes first and the JDK already ships twice; - IDENTITY_FINISH declared alongside a finisher does not fail, it makes the stream skip the finisher, so a method promising an immutable list returns a plain ArrayList in silence; - List.copyOf copies and rejects null while Collections.unmodifiableList wraps and accepts it, and Stream.toList differs from Collectors.toUnmodifiableList on exactly that point; - the one worth writing: merging into a map while reporting which keys collided, which Collectors.toMap cannot express - it can only throw or discard the loser silently. All four pieces do real work there, including the combiner, which surfaces collisions neither parallel half saw alone. The javadoc also records the counterexample: the three-way IndexPolicy split in ContentletIndexAPIImpl wants Collectors.groupingBy, not a custom collector. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds StreamGatherersDemo to the talk demos. Four sections, runnable with `java <path>.java`: - batching, where windowFixed emits the short final window on its own - the flush that hand-written accumulators forget; - scan and fold, the built-ins that carry state between elements; - writing your own, including that returning false from the integrator short-circuits the stream, which no Collector can do; - mapConcurrent, which runs each element on a virtual thread under an explicit bound and preserves encounter order. Measured 1246 ms sequential vs 347 ms at concurrency 4. The javadoc records the two call sites this lands on: OSIndexAPIImpl collects a list purely so Lists.partition can cut it up, and PopulateContentletAsJSONUtil splits the batch flush across the accumulator and its caller, duplicated for inserts and updates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SimplestGathererDidactic takes the API apart one step at a time, as the teaching companion to StreamGatherersDemo (which shows what gatherers are for, not how to write one). passThrough/onlyLong/upperCase/twice are the same stateless gatherer one line apart, making the point that the only decision is how many times you call push(). numbered/whenChanged/occurrence then introduce state and show what map and filter cannot express. Runs standalone: java dotCMS/src/test/java/com/dotcms/jdk/SimplestGathererDidactic.java Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What
Seven runnable
maindemos underdotCMS/src/test/java/com/dotcms/jdk/, built for the Java 21→25 tech talk (#34154). They are the evidence half of the talk: each one turns a claim about the JVM into something an audience can watch happen, on this codebase's own classes and its own incidents — with one exception, §6, which is teaching material for the same topic rather than evidence.They cover three of the talk's topics: carrier scheduling and pinning (Loom), object layout (JEP 519), and the stream APIs (JEP 485 plus custom collectors).
No production code is touched. Nothing new runs in CI beyond compiling — the classes are named
*Demo, so surefire skips them.1.
VirtualThreadCarrierTimelineDemo— you can watch the carriersOne row per carrier, one column per 100 ms tick, showing which of six virtual threads was mounted at that instant.
Two things make it possible without JFR or an agent:
toString().VirtualThread.toString()appends@ForkJoinPool-1-worker-Nonly while mounted (.../runnable@ForkJoinPool-1-worker-1vs.../timed_waiting). The poller has to be a platform thread — run it on a virtual one with-Dmonitor=virtualand the tool freezes mid-drawing, which is the same failure everything else is suffering.read()indefinitely.-Dmode=filecomes out identical to a CPU hog — 2 of 6 tasks mounted, 0 hand-offs, the canary never runs in 4 s — while-Dmode=socketfrees both carriers immediately.That last contrast is the point, and it is the shape of #37038 (fixed in #37041): JEP 491 stopped
synchronizedfrom pinning; it did not make file I/O unmount. Confusing the two killed content indexing until the pod was restarted.2.
VirtualThreadCarrierStarvationDemo— the blast radiusTen uninterruptible tasks, submitted either to a virtual-thread executor or a fixed pool of two platform threads. Both start exactly 2 of 10. The difference is what happens to everything else:
With platform threads the starvation is contained in your pool under your bound. With virtual threads the scarce resource is the carrier pool —
availableProcessors()wide, global to the JVM — so an unrelated task that never touched that executor starves as well.3.
VirtualThreadYieldVsParkDemoThe same two carriers, separating yielding from parking, for the part of the talk that explains why the scheduler is cooperative and nothing reclaims a carrier by force.
4.
CompactObjectHeadersDemo— measuring JEP 519 on our own records-XX:+UseCompactObjectHeadersis read once at JVM startup, so no test can compare both settings — which is exactly whyCacheSizingUtilTestwas rewritten to assert layout-independent invariants when the flag went in (78ab65083d, #35931, which had to delete its hardcoded byte counts in the same commit). This demo therefore relaunches itself in two child JVMs and prints both columns:Read the two hit rows together:
SiteSearchHitcarries one more reference thanContentSearchHit, and with 12-byte headers that field costs 0 bytes — 12+24=36 rounds up to 40, so it lands in padding already paid for. With 8-byte headers the short shape lands exactly on 32 and the same field now costs 8. Compact headers do not simply make objects 4 bytes smaller; they change which refactors are worth doing. TheSearchHitsplit (#36899) returned nothing before the flag and returns 8 bytes per hit after it.The last row keeps that honest, and is the row to end on: against a hit carrying its own
_source, the split is worth 0.25% while the flag itself is worth 5.8% — because the flag shrinks every object in the graph, the map nodes and strings and char arrays, not the record you were looking at.How it measures — two independent instruments, neither a sizing API reporting on itself:
ThreadMXBean.getCurrentThreadAllocatedBytes()around the fill loop (the headline). The JVM counts the bytes it handed out, so it is exact and does not wait on the collector. Verified reproducible: identical output at 200k, 500k and 1M instances per shape.An earlier revision used the heap delta alone and is why both are here now: it reported the
realistic hitrow as 585 B at 25k instances and 3160 B at 50k. The allocation counter does not have that failure mode.It runs two ways and gives the same numbers: standalone with
java <path>.java(falling back to local twin records, which it says in the output), or with-cp dotCMS/target/classesagainst the realContentSearchHit/SiteSearchHit.5.
StreamGatherersDemo— the extension point the middle of a stream never hadA
Stream's intermediate vocabulary closed in Java 8.Collectorslets you write your own terminal operation; nothing let you write your own intermediate one. That matters because the existing ones are amnesic —mapsees one element and produces one,filtersees one and decides keep-or-drop — so anything needing state between elements (batching, sliding windows, running totals, collapsing runs, stopping on a condition) had to leave the stream entirely.Two things this lands on, both already written by hand here:
OSIndexAPIImpl.getIndexAlias(~846-855) doesstream().map(...).collect(toList())and immediatelyLists.partition(physicalNames, ALIAS_LOOKUP_BATCH_SIZE)— the whole list exists solely so it can be cut up.Lists.partitionappears in a dozen more places.PopulateContentletAsJSONUtil(~466-471 and ~332-334) splits the batch flush across two places that must agree: the accumulator flushes atMAX_BATCH_SIZE, and the caller must rememberif (!paramsInsert.isEmpty()) doInsertBatch(...)for the remainder. Duplicated again for updates. That lone[idx7]above is the flush you cannot forget.mapConcurrentis where this topic meets Loom — each element on a virtual thread, under a bound you choose, encounter order preserved:That is the JDK's answer to "parallelise the I/O in this stream", which
parallelStream()never did well: it borrows the common ForkJoinPool, sized for CPU work and shared process-wide.6.
SimplestGathererDidactic— the same API taken apart, one step at a timeStreamGatherersDemoabove shows what gatherers are for. This one is its teaching companion and shows how to write one, starting from a gatherer that does nothing at all:That is already the whole API. The four stateless methods are the same gatherer one line apart, which makes the only real decision visible — how many times you call
push:So a gatherer subsumes the operations we already had. What makes it new is the first parameter,
state, which the four above ignore — and the three that follow do not:numbered()cannot be written withmap: the result depends on how many elements went past first.whenChanged()cannot be written withfilter: a predicate sees one element and nothing about its neighbours. The javadoc records the three rules that go with state — it must be a mutable object, never anint(a lambda cannot capture a mutable local);Gatherer.ofSequentialis what supplies it; and the integrator shouldreturn downstream.push(...)rather thanreturn true, so a downstream that has stopped asking propagates back up.7.
CustomCollectorDemo— when a customCollectoris worth writingAnatomy first (
supplier/accumulator/combiner/finisher, and whyAandRdiffer), then two traps worth seeing once:IDENTITY_FINISHsilently skips the finisher. Declaring it alongside a finisher does not fail — the stream just never calls it, so a method promising an immutable list hands back a plain mutableArrayList. Verified: the demo mutates the result and nothing complains.List.copyOfcopies and rejectsnull;Collections.unmodifiableListwraps and accepts it.Stream.toList()is immutable and null-tolerant whileCollectors.toUnmodifiableList()throws NPE — a difference that surfaces wherever an unset content field arrives asnull.The one that is worth writing merges into a map and reports which keys collided — the answer
Collectors.toMapcannot give, since it can only throw (losing every other collision) or take a merge function and discard the loser in silence:All four pieces do real work there, including the combiner, which surfaces collisions neither parallel half saw alone. The javadoc also records the counterexample: the three-way
IndexPolicysplit inContentletIndexAPIImpl.addContentToIndexusesCollectionsUtils.partitionwith positionalget(0)/get(1)/get(2), and wantsCollectors.groupingBy— not a custom collector.Testing
All seven were run end to end on JDK 25.0.2 and the module's test sources compile with the real build (
./mvnw test-compile -pl :dotcms-core --am -Dmaven.build.cache.enabled=false, exit 0; every.classfile lands intarget/test-classes).Notes for the reviewer
System.outis deliberate and is called out in each javadoc. The console output is the artifact being shown to an audience, and the demos must run withjava Foo.javaon a bare JDK with no dotCMS on the classpath, whereLoggerdoes not exist. The Logger-only Critical Rule targets production code.maindemos compiled by the real build so they cannot silently rot, and named*Demoso surefire skips them. They register in no suite by design.🤖 Generated with Claude Code
Related to: #34154
This PR fixes: #34154