Skip to content

Add AsyncDrainWriter to eliminate PrintWriter lock contention - #12654

Draft
gnodet wants to merge 1 commit into
masterfrom
async-drain-writer
Draft

Add AsyncDrainWriter to eliminate PrintWriter lock contention#12654
gnodet wants to merge 1 commit into
masterfrom
async-drain-writer

Conversation

@gnodet

@gnodet gnodet commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

During parallel model building (-T1C), multiple PhasingExecutor threads log concurrently via:

SLF4J → MavenSimpleLogger → ProjectBuildLogAppender → SimpleBuildEventListener → PrintWriter.println()

Since PrintWriter.println() is synchronized, all threads serialize on every log call. JFR profiling on a 4383-module diamond-graph project shows 1,470ms of blocked time from this single contention point — the #1 source of lock contention during validate.

Fix

AsyncDrainWriter wraps the Consumer<String> writer with a lock-free queue + non-blocking drain:

  • Producers enqueue messages into a ConcurrentLinkedQueue (CAS, no blocking)
  • At most one thread drains the queue to the underlying PrintWriter (via ReentrantLock.tryLock())
  • Other threads return immediately after enqueue — their messages are picked up by the active drain
  • close() performs a final blocking drain to guarantee no messages are lost

JFR Results (4383-module diamond project, validate -T1C)

Source Before After
PrintWriter contention 1,470ms 0ms
PhasingExecutor contention 546ms 49ms

Benchmark (10 runs)

Metric Before After Δ
Median 14.738s 14.253s -485ms (-3.3%)
Average 14.546s 14.242s -304ms (-2.1%)
Range (variance) 2.70s 1.33s Halved

All 8,766 module log lines verified present — zero messages lost.

Test plan

  • Verified 8,766/8,766 module log lines present (no lost messages)
  • JFR confirms PrintWriter contention eliminated (1,470ms → 0ms)
  • maven-core unit tests pass
  • maven-cli unit tests pass
  • CI passes

🤖 Generated with Claude Code

During parallel model building, multiple PhasingExecutor threads log
concurrently via SLF4J → ProjectBuildLogAppender → SimpleBuildEventListener
→ PrintWriter.println(). Since PrintWriter.println() is synchronized,
this creates significant contention (1,470ms blocked time on a 4383-module
project with -T1C).

AsyncDrainWriter wraps the writer Consumer with a lock-free
ConcurrentLinkedQueue and a non-blocking tryLock() drain pattern:
- Producers enqueue messages (CAS, no blocking)
- At most one thread drains the queue to the underlying PrintWriter
- Other threads return immediately after enqueue
- close() performs a final blocking drain to ensure no messages are lost

JFR results on 4383-module diamond project (validate -T1C):
- PrintWriter contention: 1,470ms → 0ms (eliminated)
- PhasingExecutor contention: 546ms → 49ms (side effect)
- Median wall time: -485ms (-3.3%)
- Run-to-run variance halved (range 2.70s → 1.33s)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.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