Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Multi-fork test execution
Adds an optional second dimension of parallelism to
deder test— run a module's testsuite across several forked JVMs, in addition to the existing in-JVM thread pool.
Motivation
Large test suites bottleneck on single-JVM contention (library-global locks, Netty
worker groups, JIT warmup on cold classes, I/O serialization). We already fork one JVM
per test task and thread-parallelize inside it; adding a second dimension — multiple
forks per module — scales throughput on machines with spare cores/memory.
Defaults are unchanged:
maxTestForks = 1per module is equivalent to today.Opt-in is a single Pkl field.
Design in brief
maxTestForksforked JVMs. Each fork continues to run its internal thread pool of size
testParallelism. Effective concurrent classes per module ≈M × testParallelism.min(maxTestForks, totalTestClasses)— no empty forks spawned.per-class duration history. Missing durations fall back to the median; first run with
no history falls through to round-robin.
Semaphorecaps concurrent forked JVMs acrossthe whole server (sized by
maxConcurrentTestForks, default =Runtime.availableProcessors()). One permit per spawned JVM, released on exit.Natural staggering when many modules ask for many forks at once; no deadlock because
permits are per-JVM, not per-task.
jvmOptionscontains a fixed-agentlib:jdwp=...port andmaxTestForks > 1, the orchestrator refuses with a clear error (multiple forkscan't all bind the same debug port). Users set
maxTestForks = 1when debugging.Output & capture
Forks talk back to the orchestrator over a JSON-lines envelope protocol on stdout
(
ForkStarted,SuiteStarted,SuiteCompleted,UnattributedOutput,ForkCompleted).Envelope lines are prefixed with
@@DEDER-FORK@@so stray pre-capture bytes are stilltolerated.
Inside each fork, a capturing
PrintStreamis installed onSystem.outbefore anylogger captures it. A
ThreadLocalbuffer accumulates whatever the suite writes — bothframework reporter output and user
printlns — and is emitted as a singleSuiteCompletedenvelope on suite completion. Writes from threads with no activesuite become
UnattributedOutputenvelopes, newline-flushed to keep them ordered.The orchestrator renders each
SuiteCompletedas a single block in the terminal:=== SuiteName ===
<captured output including ScalaTest's reporter lines and any prints>
When
effectiveForks > 1, every header/line is tagged[fork-N]. With a single forkthere is no tag — the output is indistinguishable from the classic single-JVM view
except for the per-suite grouping.
Per-suite batching trade-off
A suite's stdout is held until the suite completes. A long-running suite won't stream
live output mid-run. This was an explicit choice (discussed in the design doc). A
size/time-based partial flush can be layered on the same envelope stream later without
changing the protocol.
PASS lines dropped
DederTestEventHandlerno longer emits aPASS ✅line per test; frameworks (ScalaTest,munit, utest, JUnit, etc.) already print passes through their own reporters, so we were
double-logging.
FAIL 🔴/SKIP 🚫lines and failure stack traces are kept — deder'svalue-add is a uniform failure surface across frameworks.
Run & history persistence
Each invocation creates
.deder/out/<moduleId>/test/run-<YYYYMMDD-HHmmss-SSS>-<uuid4>/containing one subdirectory per fork (
fork-0/,fork-1/, …). Each fork directoryholds its
fork-args.json, per-forkfork-results-*.jsonpayload, andstdout.log/stderr.logfor offline inspection. Previous runs are preserved.At the module level,
.deder/out/<moduleId>/test/test-history.jsoncarries cumulativeMap[className, TestClassStats](duration + last status + last-run epoch). It's loadedat orchestrator start, merged with the current run's stats at the end, and atomically
rewritten via tmp-file + rename. Corrupt or missing file → empty map; never fatal.
No database, no schema migrations — a single JSON file per module. If dynamic
work-stealing or cross-module analytics become needs later, the on-disk format can be
migrated without touching call sites.
Cancellation
The orchestrator checks
DederGlobals.cancellationTokensbefore acquiring each permit(queued forks abort without launching) and calls
proc.wrapped.destroyForcibly()onrunning forks when cancelled. No cooperative drain — forks die. Results from suites
already flushed are preserved; partial suites are lost.
Configuration
Per module (
config/DederProject.pkl)Added to
JavaTestModuleandScalaTestModule: