Skip to content

Feat/add CFS scheduler - #1794

Open
panicking wants to merge 7 commits into
jenkinsci:masterfrom
panicking:feat/add-performance-improvement
Open

Feat/add CFS scheduler#1794
panicking wants to merge 7 commits into
jenkinsci:masterfrom
panicking:feat/add-performance-improvement

Conversation

@panicking

@panicking panicking commented Jun 18, 2026

Copy link
Copy Markdown

Add CFS (Completely Fair Scheduler) to CPS VM thread execution

Motivation

The CPS VM's CpsThreadGroup.run() currently executes all runnable threads in insertion (ID) order to exhaustion in a single pass. A standing // TODO at that method acknowledges the problem: a tight Groovy loop that rarely suspends can monopolize the CPS VM thread, delaying all other pipeline branches in the same build. Later-created threads (higher IDs) always execute after earlier ones — there is no fairness guarantee.

Changes

Two commits that build on each other:

1. Performance Instrumentation (CpsFlowExecution)

Adds measurement hooks so every subsequent optimization can be benchmarked:

  • 4 new TimingKind values: serializationRead, serializationWrite, compilationTotal, schedulerWait — recorded during normal pipeline execution and visible in thread dumps and support bundles.
  • Per-thread execution counters on CpsThread: totalChunksRun, totalNanosExecuting, becameRunnableAt — foundation for vruntime tracking.
  • Per-pass scheduler metrics on CpsThreadGroup: thread counts, runnable counts, pass duration, queue depth — logged at FINE level.

Zero behavioral change. All new fields are either transient (diagnostic) or primitive long (transparent to River serialization).

2. CFS Scheduler (CpsThreadGroup.run())

Replaces the "run all runnable threads" loop with a fairness algorithm adapted from Linux's Completely Fair Scheduler:

  • Each CpsThread accumulates virtual runtime (vruntime) proportional to its wall-clock execution time divided by weight:
    vruntime += (elapsedNs * 1024) / max(1, weight)
    
  • Each scheduler pass picks the thread with the lowest vruntime instead of iterating all threads in ID order.
  • New threads start at min_vruntime so they get prompt attention rather than being starved.
  • A dynamic quantum scales with system load:
    quantum = 50ms / max(1, activeBuilds / loadFactor)    floor = 5ms
    

The quantum is cooperative — checked after each CPS chunk completes, never during execution. Nearly all chunks suspend naturally at step boundaries (sleep, sh, semaphore) in microseconds, so the 50ms default generates zero additional overhead. The real fairness comes from vruntime ordering: CPU-heavy threads accumulate higher vruntime and naturally lose priority on subsequent passes.

Configuration

Property Type Default Effect
org.jenkinsci.plugins.workflow.cps.CFS.baseQuantumMs long (ms) 50 Maximum wall-clock time per thread per pass. Set to 0 to disable quantum enforcement while keeping CFS ordering.
org.jenkinsci.plugins.workflow.cps.CFS.loadFactor int 10 Controls how aggressively the quantum shrinks under load. Lower = shrinks sooner.

Example JVM arguments:

-Dorg.jenkinsci.plugins.workflow.cps.CFS.baseQuantumMs=100
-Dorg.jenkinsci.plugins.workflow.cps.CFS.loadFactor=20

Files Changed

 doc/cfs-scheduler.md                               | 153 ++++++++++++
 .../jenkinsci/plugins/workflow/cps/CpsFlowExecution.java     |  26 ++-
 .../jenkinsci/plugins/workflow/cps/CpsThread.java  |  52 ++++-
 .../jenkinsci/plugins/workflow/cps/CpsThreadGroup.java       | 235 ++++++++++++++-----
 .../workflow/cps/CpsFlowExecutionTest.java         |   4 +-
 5 files changed, 412 insertions(+), 58 deletions(-)

How It Compares to Before

Aspect Before After
Thread selection All runnable threads, ID order One thread, lowest vruntime
Fairness Later threads (higher IDs) always run last Equal share among all threads
New threads Starved until existing threads complete Start at min_vruntime
Scheduler wait Measured but not acted on Measured and used for ordering
Configuration None Two system properties
Per-pass overhead O(n) scan + execute all runnable O(n) scan + execute one thread

Backward Compatibility

The new vruntime (long) and weight (int) fields on CpsThread are Java primitives, handled automatically by the existing River serialization. Old program.dat files deserialize with vruntime=0 and weight=0; the resume() method corrects weight=0 to the default 1024 on first use. No migration is needed.

Testing

All 79 CPS tests pass:

CpsFlowExecutionTest:        31 ✓
CpsThreadDumpTest:             4 ✓
CpsThreadTest:                 1 ✓
CpsScriptTest:                 7 ✓
SandboxContinuableTest:        1 ✓
ReplayActionTest:              9 ✓
ReplayPipelineCommandTest:     1 ✓
LoadStepTest:                  3 ✓
ParallelStepTest:             15 ✓
RestartingLoadStepTest:        7 ✓

Serialization round-trips are verified by the restart tests (RestartingLoadStepTest, ReplayActionTest).

Documentation

Complete documentation in doc/cfs-scheduler.md covering:

  • vruntime tracking and scheduling weight
  • Fair queue selection algorithm
  • New thread initialization
  • Dynamic quantum formula with scaling examples
  • Configuration via system properties
  • Performance characteristics
  • Observability (FINE-level logging, schedulerWait timing)
  • Future enhancement directions
    Deployment in our infrastructure for more testing

Submitter checklist

  • Make sure you are opening from a topic/feature/bugfix branch (right side) and not your main branch!
  • Ensure that the pull request title represents the desired changelog entry
  • Please describe what you did
  • Link to relevant issues in GitHub or Jira
  • Link to relevant pull requests, esp. upstream and downstream changes
  • Ensure you have provided tests that demonstrate the feature works or the issue is fixed

Add timing hooks for scheduler wait time, serialization read/write
duration, and end-to-end compilation wall-clock. Instrument CpsThread
with per-thread execution counters (totalChunksRun, totalNanosExecuting,
becameRunnableAt) and CpsThreadGroup with per-pass scheduler metrics.

Extend the TimingKind enum with four new categories:
- serializationRead  — River deserialization time in loadProgramAsync
- serializationWrite — River writeObject time in saveProgram
- compilationTotal  — parseScript end-to-end duration
- schedulerWait     — thread wait time in the run() loop

All new counters are exported via the existing support-bundle framework
and visible in thread dump output. Zero behavioral change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
@panicking panicking changed the title Feat/add performance improvement Feat/add CFS scheduler Jun 18, 2026
panicking and others added 3 commits June 18, 2026 08:13
Replace the round-robin "run all threads to exhaustion" loop in
CpsThreadGroup.run() with a Completely Fair Scheduler that picks
the thread with the lowest vruntime each pass. Threads accumulate
vruntime proportional to their execution time divided by weight.

Key changes:
- Add vruntime (long) and weight (int=1024) fields to CpsThread
- Update vruntime after each chunk: vruntime += (elapsed*1024)/max(1,weight)
- New threads start at min_vruntime for prompt scheduling
- Guards against deserialized weight=0 from old program.dat
- Rewrite run() to select one thread via priority queue (vruntime min)
- Track scheduler-wait time, reset becameRunnableAt to prevent double-count
- Dynamic quantum: baseQuantum / max(1, activeBuilds/loadFactor), floor 5ms
- Configurable via system properties:
  org.jenkinsci.plugins.workflow.cps.CFS.baseQuantumMs (default 50ms)
  org.jenkinsci.plugins.workflow.cps.CFS.loadFactor (default 10)

All error handling, completion, and cleanup logic preserved verbatim.
Test assertions updated for order-independent FlowNode IDs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
Document the Completely Fair Scheduler algorithm for CPS thread
execution, including vruntime tracking, scheduling weight, dynamic
quantum computation, configuration via system properties, comparison
to the original scheduler, and future enhancement directions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
Add explicit explanation that the quantum is checked after each CPS
chunk completes, never mid-execution. Document that 50ms default
generates zero additional overhead since nearly all chunks suspend
naturally in microseconds. Add Performance Considerations section
covering overhead and serialization impact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
@panicking
panicking force-pushed the feat/add-performance-improvement branch from 67862e7 to e040b59 Compare June 18, 2026 06:14
panicking and others added 3 commits June 18, 2026 17:31
Add hierarchical two-level vruntime tracking modeled after Linux CFS group
scheduling to ensure fair CPU distribution across multiple pipeline builds
sharing the same master thread pool.

Three mechanisms work together:

1. Flow vruntime tracking (CpsFlowExecution):
   - Each pipeline build accumulates flowVruntime, representing total CPU
     consumption of all its threads.
   - flowWeight (default 1024, NICE_0_LOAD) scales accumulation rate.
   - getGlobalMinFlowVruntime() scans active builds for the minimum.

2. Thread initialization floor (CpsThread):
   - New threads inherit max(groupMinVruntime, flowVruntime) instead of
     just groupMinVruntime, preventing CPU-heavy pipelines from getting
     unfairly low vruntime on new threads.

3. Flow-level quantum adjustment (CpsThreadGroup):
   - computeQuantumNs() now scans FlowExecutionList for global minimum
     flowVruntime. Flows exceeding the minimum get proportionally reduced
     quantum, bounded at 25% of base.
   - Controlled by new system property:
     org.jenkinsci.plugins.workflow.cps.CFS.flowFairnessFactor
     (int, range 0-100, default 50). 0 disables quantum adjustment
     while keeping vruntime tracking active.

Observability: scheduler pass logs and CpsThread.toString() now include
flow vruntime and weight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
9 tests covering:
- flowVruntime advances when threads execute
- flowVruntime increases with more work
- flow weight defaults and scaling behavior
- thread toString includes flow vruntime/weight
- getGlobalMinFlowVruntime correctness
- concurrent builds have independent flow vruntime tracking
- quantum adjustment with non-zero flowFairnessFactor
- quantum adjustment disabled with flowFairnessFactor=0
- heavy vs light pipeline vruntime accumulation comparison

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
Move pipeline-level fairness from 'Future Enhancements' to a fully
documented feature. Add sections covering:

- Flow vruntime accumulation mechanism
- New thread initialization with flow floor
- Flow-level quantum adjustment formula
- Flow weight configuration
- flowFairnessFactor system property reference
- Updated observability section with flowVr in scheduler logs
  and CpsThread.toString() output

Promote cross-pipeline fairness to implemented; remaining future
enhancements are thread weight boosting, FlowHead affinity, and
CFS time slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Michael Trimarchi <michael@amarulasolutions.com>
@panicking
panicking force-pushed the feat/add-performance-improvement branch from d0964e6 to 9c1358d Compare June 18, 2026 17:40
@panicking

Copy link
Copy Markdown
Author

@jglick can someone take a look? I'm testing it but I don't know is somenthing we want to explore or have

@jglick

jglick commented Jul 21, 2026

Copy link
Copy Markdown
Member

If you are referring to

// TODO: maybe instead of running all the thread, run just one thread in round robin
this dates to 95e7372 in #46 from a decade ago. I never expected this code to be touched again. Is there a concrete problem you are trying to solve here?

a tight Groovy loop that rarely suspends

suggests poorly written Pipeline script; Groovy code should be completing on the order of milliseconds.

@panicking

Copy link
Copy Markdown
Author

If you are referring to

// TODO: maybe instead of running all the thread, run just one thread in round robin

this dates to 95e7372 in #46 from a decade ago. I never expected this code to be touched again. Is there a concrete problem you are trying to solve here?

a tight Groovy loop that rarely suspends

suggests poorly written Pipeline script; Groovy code should be completing on the order of milliseconds.

Yes, I have started from a TODO, so what you need are metrics that are showing some improvements at different loads. Right now, I'm using in my infrastructure but I can prepare some metrics

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.

2 participants