Repository navigation
[Test][Zeta] Add a checkpoint scheduling delay benchmark - #12418
Conversation
CheckpointingTimeBenchmark cannot see how a checkpoint trigger was scheduled: CheckpointBenchmarkTrigger reflects straight into createPendingCheckpoint and startTriggerPendingCheckpoint, so the scheduler is out of the measured path, and the number it reports is dominated by the barrier round-trip in any case. CheckpointSchedulingBenchmark measures the scheduling layer instead: the delay between a checkpoint trigger becoming due and its scheduling thread running it, sampled as a distribution. The axis it sweeps is the pipeline count, because every CheckpointCoordinator builds its own two-thread pool and a member running P pipelines therefore carries 2P scheduler threads. How long a trigger occupies its thread is a parameter rather than a fixed cost, since that is what decides whether a given pool width is wide enough and it is not the same for every deployment.
| @Fork( | ||
| value = 3, | ||
| jvmArgsAppend = { | ||
| "-Xms2g", |
There was a problem hiding this comment.
please use:
"-Xms4g",
"-Xmx4g",
There was a problem hiding this comment.
Done in e8a8450b0 - 4g, matching CheckpointingTimeBenchmark.
| - `pipelineNum`: pipelines scheduling checkpoints on the member. Each `CheckpointCoordinator` | ||
| builds its own two-thread pool, so this also sets the scheduler thread count. | ||
| - `checkpointIntervalMillis`: interval of the background trigger load. The default is far below | ||
| the production default of 300000 so that a short iteration sees a realistic number of triggers. |
There was a problem hiding this comment.
The benchmark testing document has been refactored and does not require any changes
There was a problem hiding this comment.
Reverted in 556de1f92 - README is back to dev untouched. I had missed that the benchmark documentation moved to docs/en/engines/zeta/benchmark.md; that file is unchanged too.
…flow Adds CheckpointSchedulingBenchmark to the benchmark suite choices so it can be dispatched from the Benchmarks workflow like the other benchmark classes. Reverts the README change: benchmark documentation now lives in docs/en/engines/zeta/benchmark.md and needs no per-benchmark section here.
|
All three items are pushed.
One judgement call I did not make on my own: I did not add it to If you want it tracked nightly the options are to take that cost, or to cut the I am running the full sweep on |
DanielLeens
left a comment
There was a problem hiding this comment.
Thanks a lot for this contribution, @Vivek1106-04, and for turning around the three items from @nzw921rx so quickly. The idea is a good one: the checkpoint scheduling layer really is a blind spot today (CheckpointingTimeBenchmark reflects straight into the trigger and never touches the ScheduledExecutorService), and having a distribution-style (SampleTime) benchmark for it will help the discussion in #12339 and #12165 a lot. The code is clean, follows the existing *Benchmark + *BenchmarkState delegate pattern, carries the ASF headers, and CI is green. I have no hard blockers, but I found one design point that affects how far the resulting numbers can be trusted, plus two small test and reporting points.
What Problem Does This PR Solve?
- User pain point: there is no way to measure how long a due checkpoint trigger waits for its scheduling thread. Every
CheckpointCoordinatorcreates its ownExecutors.newScheduledThreadPool(2, ...)(CheckpointCoordinator.java:240-247), and the proposal in #12339 wants to replace that with a member-wide scheduler. Without a benchmark, the two models can only be argued about. - Fix approach: add
CheckpointSchedulingBenchmark(JMHMode.SampleTime, microseconds) plus aCheckpointSchedulingBenchmarkStatethat builds one 2-thread scheduler per pipeline, arms a periodic "trigger body" on each, and measures the round trip of a zero-delay task submitted to pipeline 0's scheduler. It sweepspipelineNum(1/10/100/500) andtriggerBodyMicros(0/500), registers the class in thebenchmarks.ymlchoice list, and adds a small unit test for the harness. - One-sentence summary: benchmark-only harness that samples per-pipeline scheduler wake-up delay so a shared scheduler can later be compared on identical settings.
Before / after: before, -Pbenchmark had no class that exercises the scheduling threads; after, CheckpointSchedulingBenchmark.perPipelineSchedulerTriggerDelay reports p50/p99/max for each (pipelineNum, triggerBodyMicros) pair and can be selected from the benchmarks dropdown.
1. Code Change Review
1.1 Core Logic Analysis
What changed (4 files, +454/-0, all under seatunnel-benchmarks/ plus one line in .github/workflows/benchmarks.yml; no engine, connector or API code):
CheckpointSchedulingBenchmark.java: JMH entry point with onePerPipelineSchedulerState(@Setup(Level.Trial)/@TearDown(Level.Trial)) that delegates to the state class. JVM args (-Xms4g -Xmx4g -XX:+UseG1GC -XX:+AlwaysPreTouch ...) are identical toCheckpointingTimeBenchmark, so the heap request from @nzw921rx is satisfied.CheckpointSchedulingBenchmarkState.java: createspipelineNumScheduledThreadPoolExecutor(2)instances named like the real coordinator threads, armsscheduleWithFixedDelay(runTriggerBody, staggeredInitialDelay, checkpointIntervalMillis)on each, and exposesscheduleAndAwaitTrigger()which doesschedulers.get(0).schedule(latch::countDown, 0, ms)and waits on the latch (30 s guard).CheckpointSchedulingBenchmarkTest.java: 4 tests (annotation contract, 200 triggers under load, teardown leaves no thread, parameter validation).benchmarks.yml: adds'CheckpointSchedulingBenchmark'to thebenchmarkschoice list (line 43).
Key findings:
- The harness mirrors the real pool shape correctly: 2 core threads,
setRemoveOnCancelPolicy(true), same thread-name prefix (CheckpointCoordinator.java:240-248vsCheckpointSchedulingBenchmarkState.java:152-167). The one thing it leaves out is theMDCTracer.tracing(...)wrapper, which is fine for a scheduling-latency measurement. - Parameter validation matches the engine (
MIN_CHECKPOINT_INTERVAL_MILLIS = 10matchesCheckpointConfig.java:57). - Lifecycle is sound:
shutdownNow()thenawaitTermination()per pool, background failures are surfaced through anAtomicReferenceand rethrown on the next measured call. tools/benchmarks/run_benchmarks.shaccepts a bare class name as selector, so the new dropdown entry works without further wiring.- The measured path, however, cannot exhibit queueing in the per-pipeline model (see Issue 1).
Correctness analysis of the measured path (no engine lifecycle is touched, so no runtime-path block is needed): scheduleAndAwaitTrigger() -> ScheduledThreadPoolExecutor.schedule() -> DelayedWorkQueue -> idle worker of pool 0 wakes -> latch.countDown() -> JMH thread wakes. Each pool has 2 workers and exactly one periodic body (0 or 500 microseconds once per 1000 ms). The measured task therefore always finds an idle worker, and the sample is essentially one thread hand-off.
Evidence from the fork CI run for this head (benchmark-test (8, ubuntu-latest), 1 fork, no warmup, 1 iteration of 1 s per combination): p50 is 38.1 us (pipelineNum=1, body 0), 37.8 us (1, 500), 36.8 / 36.9 us (10), 29.8 / 29.1 us (100), 33.5 / 32.6 us (500). That is flat across both parameters. This is a short single-fork run, so it is only supporting evidence; the conclusion above comes from the code.
1.2 Compatibility Impact
Fully compatible. Test-scope code plus a workflow_dispatch choice entry; no config option, default, serialization, checkpoint/savepoint or SPI change, and no change to any existing benchmark or to the nightly suite.
1.3 Performance / Side-Effect Analysis
- Runtime cost of the benchmark itself:
@Fork(3)from the class,BenchmarkBasedefaults of 3 warmup and 5 measurement iterations of 10 s, 8 parameter combinations. That matches the author's estimate of roughly 4 minutes per combination, about 32 minutes per Java version if run in full. It is not part ofbenchmarks_core.txt, so nightly runs are unaffected. I agree with the decision not to add it to the nightly suite for now; see Issue 1 for why the matrix is currently larger than the information it can produce. - Resource footprint: 500 pipelines means 500 to about 1000 threads inside a fork that already has
-Xms4g -XX:+AlwaysPreTouch. The CI validation run completed all 8 combinations in a few seconds each, so this is fine on ubuntu-latest. - The unit test finishes in 0.045 s in the fork CI log (Java 8 job), so the added CI time is negligible. Nothing new runs in the default
unit-testjob, since the module is behind-Pbenchmark.
1.4 Error Handling and Logging
No swallowed exceptions in the main paths. runTriggerBody re-interrupts on InterruptedException and records other throwables for the next measured call; the 30 s latch guard and 10 s shutdown guard fail loudly. Formal issues:
Issue 1: The per-pipeline baseline cannot show queueing, so triggerBodyMicros (and largely pipelineNum) do not influence the measured sample
- Location:
CheckpointSchedulingBenchmarkState.java:78-93(one periodic body per pool),:104-117(measured task always goes toschedulers.get(0)),CheckpointSchedulingBenchmark.java:94-101(parameter matrix). - Problem: each pool has 2 threads and only one periodic body, with a duty cycle of at most 500 us per 1000 ms. The measured task can only wait if both threads of pool 0 are busy, which cannot happen. Other pipelines live in other pools and only compete for CPU. The real coordinator can hold both threads (a blocked trigger plus a blocked timeout watchdog,
CheckpointCoordinator.java:550and:1029), but the harness never schedules a second task kind per pool. As a result, the number that comes out is thread wake-up latency, and thetriggerBodyMicrossweep (half of the 8 combinations) is not expected to move it. The fork CI numbers quoted in 1.1 agree with this. Note also that the PR description says the parameter "decides whether a given pool width is wide enough", which is only true once a model in which timers and bodies share threads exists. - Potential risk: the full sweep the author plans to post for
devwill look flat, and that flatness can be read as "the current model has no scheduling delay", which is not what was actually measured. It also spends about 16 of the 32 minutes per Java version on combinations that cannot differ. - Best improvement: pick whichever fits the plan for #12165. (a) Keep the harness as the deliberate control for a later shared-scheduler state, and say so in the class Javadoc ("expected to be flat; the axis becomes meaningful once bodies and timers share threads"), and trim the default matrix (for example
triggerBodyMicros= 500 only) until then. Or (b) make the baseline faithful: arm both a trigger-like and a watchdog-like task per pool, with a body long enough that both workers can be occupied, so a measured task can actually queue. - Severity: Medium (non-blocking, but worth settling before the numbers are used to judge #12165)
- Raised by another reviewer: No
Issue 2: Teardown assertion scans process-wide thread names and one test lacks a finally
- Location:
CheckpointSchedulingBenchmarkTest.java:79-90(line 89),CheckpointSchedulingBenchmarkState.java:146-150. - Problem:
shouldStopEverySchedulerThreadOnTearDownassertsThread.getAllStackTraces()contains zero threads whose name starts withcheckpoint-coordinator-. Two weaknesses: (a) the scan is JVM-wide, and the same surefire JVM also runsSeaTunnelEnvironmentContextTest, which starts a real mini cluster and executes jobs whose realCheckpointCoordinators use exactly the same thread-name prefix (CheckpointCoordinator.java:245-246); if any of those threads is still winding down when this test runs, the assertion fails through no fault of this class. (b)awaitTerminationreturns when the pool state becomes TERMINATED, which happens just before the last worker thread actually exits, so an immediate scan can very rarely still see a live thread. Separately, the test at lines 79-90 callstearDown()withouttry/finally, so a failure inscheduleAndAwaitTrigger()leaves up to 8 non-daemon pool threads behind (test 2 already does this correctly at lines 64-66). - Potential risk: low-probability flake in the
benchmark-testjob that runs for every PR touchingseatunnel-benchmarks/**. I could not reproduce or confirm either race from static reading, which is why this is Low. - Best improvement: have the state's
ThreadFactoryrecord the threads it creates, and in the testjoin(timeout)on those and assert!isAlive(). That removes the global scan and closes the exit race in one go. Wrap the body of the test intry/finallylike test 2. - Severity: Low
- Raised by another reviewer: No
Issue 3: The reported scheduler thread count is timing dependent, and is printed with System.out
- Location:
CheckpointSchedulingBenchmark.java:111-113,CheckpointSchedulingBenchmarkState.java:146-150, class JavadocCheckpointSchedulingBenchmark.java:47-49. - Problem:
ThreadPoolExecutorstarts core workers lazily, andScheduledFutureTaskstarts the second worker only after a periodic task has run once. Immediately aftersetUp()the count is therefore between P and 2P, depending on how many staggered periodic triggers have already fired. The fork CI log shows exactly that:1 pipelines: 1,10 pipelines: 11,100 pipelines: 102,500 pipelines: 542. The Javadoc and PR text say "2P scheduler threads", and #12339 itself says "up to two". Also,System.out.printfis the only stdout write inseatunnel-benchmarks/src/main. - Potential risk: the "cost the shared scheduler exists to remove" is exactly the number this line is meant to report, and it will be non-reproducible between runs (and understated versus the worst case).
- Best improvement: either call
prestartAllCoreThreads()on each pool so the count is deterministically 2P and say "up to 2P" in the Javadoc, or report the count as a secondary JMH result instead of printing it. - Severity: Low
- Raised by another reviewer: No
2. Code Quality Assessment
2.1 Coding Standards
Good. ASF license header on all three new files, no wildcard imports, comments explain why (sleep versus spin, why the measured task only signals, why the interval defaults far below 300000). The state class is package-private and small, and it follows the delegate pattern used by IntermediateQueueBenchmark. Nit: the private helper awaitEveryPipelineTriggered sits between test methods (CheckpointSchedulingBenchmarkTest.java:92-104); moving it below the last test would match the usual layout.
2.2 Test Coverage and Test Stability
Coverage of the harness is appropriate: main path (200 measured triggers under 8 pipelines with a 20 ms interval), lifecycle (teardown), and parameter validation for all three inputs. The first test only re-asserts class-level annotations, which mirrors the existing CheckpointingTimeBenchmarkTest pattern, so that is consistent with the module.
Stability rating: Risk present.
- Hard sleeps: only the 20 ms polling loop bounded by a 30 s deadline (
CheckpointSchedulingBenchmarkTest.java:98-104) and the intentional body sleep inside the background load. No latency thresholds are asserted, so the test cannot flake on machine speed. - Lower-bound assertions (
>= PIPELINE_NUM) are safe: worker prestart on the firstscheduleWithFixedDelayguarantees one live thread per pool. - No fixed ports and no static state;
state.tearDown()runs infinallyfor test 2. - Risk items: the JVM-wide thread-name scan and the missing
finallyin test 3 (Issue 2,:89and:84-87). - Where it runs: only in
benchmark-test (8|11, ubuntu-latest), which passed on this head (58 tests, 0 failures in the Java 8 log, including all 4 new tests). The defaultunit-testjob is skipped for this module.
2.3 Documentation Updates
No user-facing change, and I agree with the decision to leave seatunnel-benchmarks/README.md untouched after the benchmark docs moved to docs/en/engines/zeta/benchmark.md (that file does not enumerate benchmark classes, so nothing is out of date). If Issue 1 is taken in direction (a), the class Javadoc is the right place for the "expected to be flat" note.
3. Architectural Soundness
3.1 Elegance of the Solution
Long-term solution in intent, and the state/benchmark split is neat. As written it is a faithful model of the pool shape but not yet of the load shape (Issue 1). Isolating the scheduling layer instead of going through a real CheckpointCoordinator is the right call for this quantity.
3.2 Maintainability
Good. The harness has no dependency on engine internals beyond documented constants (pool width, thread-name prefix, minimum interval); if the engine changes those, the constants and Javadoc need to follow, which the comments already point out.
3.3 Extensibility
The PerPipelineSchedulerState / perPipelineSchedulerTriggerDelay naming clearly leaves room for a shared-scheduler state, which is the natural next step for #12165. Keeping the measured-task contract (scheduleAndAwaitTrigger) behind the delegate will make that comparison straightforward.
3.4 Historical-Version Compatibility
Not affected. No config, wire format, state or checkpoint format is touched; nothing here is on the 2.6 -> 3.x style upgrade path since it is benchmark code only. Existing benchmarks and the benchmarks_core suite are unchanged.
4. Issue Summary
| # | Issue | Location | Severity |
|---|---|---|---|
| 1 | Baseline harness cannot show queueing; triggerBodyMicros and pipelineNum do not influence the measured sample |
CheckpointSchedulingBenchmarkState.java:78-93,104-117, CheckpointSchedulingBenchmark.java:94-101 |
Medium |
| 2 | Teardown assertion scans JVM-wide thread names; test 3 lacks try/finally |
CheckpointSchedulingBenchmarkTest.java:79-90 |
Low |
| 3 | Reported scheduler thread count is timing dependent (P to 2P) and printed via System.out |
CheckpointSchedulingBenchmark.java:111-113 |
Low |
No High-severity issues. CI status: the fork Build run and the benchmark-test jobs for Java 8 and 11 succeeded on this head.
5. Merge Recommendation
Conclusion: Ready to merge after fixes
- Blockers - must be fixed: none.
- Recommended fixes - non-blocking:
- Issue 1: either document that the per-pipeline baseline is the expected-flat control and trim the default matrix, or make the load faithful so a measured task can actually queue. I would settle this before posting the full sweep, so the published numbers say what they measure.
- Issue 2: track the pool threads in the state and
jointhem instead of scanning all JVM threads; add the missingfinally. - Issue 3:
prestartAllCoreThreads()(or a secondary JMH metric) so the thread count is deterministic.
Overall assessment: a small, well-structured and well-commented harness that fills a real gap, with a green CI. The remaining points are about making the measurement say what the PR text says it says; none of them is a correctness risk for the engine.
Better alternative? Option A (small): keep the harness and add the Javadoc clarification plus a smaller default matrix. Option B (more faithful): model both the trigger and the timeout watchdog per pool so the baseline can queue, then reuse the same measured-task contract for a shared-scheduler state. I lean to B if the sweep is meant to be the evidence for #12165, and A if the intent is only to land a reusable control first.
Response to the existing review: @nzw921rx's change request had three items, and all three look addressed at this head: heap set to 4g in CheckpointSchedulingBenchmark.java:62-63 (same as CheckpointingTimeBenchmark), README no longer in the diff, and the class is in the benchmarks.yml choice list (line 43). The decision on adding it to benchmarks_core.txt belongs to the maintainers; my view is to leave it out until Issue 1 is settled and the matrix is smaller. @nzw921rx, could you take another look when convenient?
Thanks again for the careful work, and for being upfront that the posted numbers are a smoke run.
|
Full sweep on this branch, replacing the smoke numbers in the description.
Errors on the means are 0.003 to 0.086 us. The benchmark resolves the thing it The I ran the same benchmark with a second arm for the #12165 shared scheduler, and These are single-machine numbers on JDK 17, while the workflow matrix is Java 8 |
nzw921rx
left a comment
There was a problem hiding this comment.
It appears that this benchmark only simulates a single pipeline occupying two threads and then performs a benchmark run. It does not exercise the current engine execution path. Therefore, I do not think it can accurately compare the performance improvement between the old and new implementations, nor can it serve as a reliable long-term performance baseline.
Enhancing CheckpointCoordinator#scheduleTriggerPendingCheckpoint would be a reasonable approach. We could introduce an @VisibleForTesting method that allows the benchmark to trigger checkpoint scheduling through the actual engine path. Otherwise, it would be extremely difficult to evaluate this accurately without making intrusive changes to the engine.
|
Thanks both for keeping this moving. @Vivek1106-04, the full sweep is useful and it actually lines up with Issue 1 from my earlier review: @nzw921rx, I think you're right that the harness measures thread wake-up delay rather than queueing under the engine's real trigger pattern, and that this limits how far the numbers can be trusted as a stand-in for the actual scheduling model comparison - that's the same gap I flagged as Issue 1 (non-blocking) in my review. On routing through the real path: there's already a testing hook at that boundary - A cheaper middle ground that gets at the same question without that machinery: my Issue 1 write-up already proposed arming both a trigger-like task and a watchdog-like task per pool with a duty cycle long enough that both of a pool's 2 workers can be occupied, so the measured task genuinely has to queue behind them - closer to how Could you say which of these you're looking for: exercising |
@DanielLeens Thank you for your reply Design: Checkpoint Scheduling Benchmark DesignGoalMeasure the scheduling delay of the real The benchmark must not override Measurement BoundaryThe remaining checkpoint path continues outside the measured region: CheckpointCoordinator ConstructorThe production constructor delegates to a testing constructor that accepts the real scheduler. public CheckpointCoordinator(
CheckpointManager manager,
CheckpointStorage checkpointStorage,
CheckpointConfig checkpointConfig,
long jobId,
CheckpointPlan plan,
CheckpointIDCounter checkpointIdCounter,
PipelineState pipelineState,
ExecutorService executorService,
IMap<Object, Object> runningJobStateIMap,
boolean isStartWithSavePoint,
CheckpointMonitorService checkpointMonitorService) {
this(
manager,
checkpointStorage,
checkpointConfig,
jobId,
plan,
checkpointIdCounter,
pipelineState,
executorService,
runningJobStateIMap,
isStartWithSavePoint,
checkpointMonitorService,
manager.leaseCheckpointScheduler(plan.getPipelineId()));
}@VisibleForTesting
protected CheckpointCoordinator(
CheckpointManager manager,
CheckpointStorage checkpointStorage,
CheckpointConfig checkpointConfig,
long jobId,
CheckpointPlan plan,
CheckpointIDCounter checkpointIdCounter,
PipelineState pipelineState,
ExecutorService executorService,
IMap<Object, Object> runningJobStateIMap,
boolean isStartWithSavePoint,
CheckpointMonitorService checkpointMonitorService,
PipelineCheckpointScheduler scheduler) {
// Keep the existing initialization unchanged.
this.executorService = executorService;
this.checkpointManager = manager;
this.checkpointStorage = checkpointStorage;
this.checkpointIdCounter = checkpointIdCounter;
this.runningJobStateIMap = runningJobStateIMap;
this.checkpointMonitorService = checkpointMonitorService;
this.scheduler = scheduler;
// Initialize the remaining existing fields normally.
}On Scheduling ProbeUse the constructor-injected final class ProbingExecutorService extends AbstractExecutorService {
private final ExecutorService delegate;
private final AtomicReference<CountDownLatch> nextProbe =
new AtomicReference<>();
ProbingExecutorService(ExecutorService delegate) {
this.delegate = delegate;
}
void arm(CountDownLatch probe) {
nextProbe.set(probe);
}
@Override
public void execute(Runnable command) {
CountDownLatch probe = nextProbe.getAndSet(null);
if (probe != null) {
probe.countDown();
}
delegate.execute(command);
}
// Delegate shutdown and lifecycle methods to delegate.
}The probe is armed only after coordinator initialization and task readiness have completed. Benchmark CoordinatorThe benchmark subclass only exposes the protected scheduling method. final class BenchmarkCheckpointCoordinator
extends CheckpointCoordinator {
void scheduleForBenchmark() {
scheduleTriggerPendingCheckpoint(
CheckpointType.CHECKPOINT_TYPE,
0L);
}
}It must not override: tryTriggerPendingCheckpoint(...)Therefore all existing checks, locking and pending-checkpoint behavior remain active. FixtureThe Fixture uses real scheduling implementations and local in-memory replacements for external services. The memory-backed get()
set()
compute()
remove()Example: ConcurrentMap<Object, Object> state =
new ConcurrentHashMap<>();
IMap<Object, Object> imap = mock(IMap.class);
when(imap.get(any()))
.thenAnswer(invocation ->
state.get(invocation.getArgument(0)));
doAnswer(invocation -> {
state.put(
invocation.getArgument(0),
invocation.getArgument(1));
return null;
})
.when(imap)
.set(any(), any());
when(imap.compute(any(), any()))
.thenAnswer(invocation -> {
Object key = invocation.getArgument(0);
BiFunction<Object, Object, Object> function =
invocation.getArgument(1);
return state.compute(key, function);
});
when(imap.remove(any()))
.thenAnswer(invocation ->
state.remove(invocation.getArgument(0)));The RPC fake returns a completed The Fixture must report all tasks as Benchmark InvocationCountDownLatch triggerSubmitted = new CountDownLatch(1);
probingExecutor.arm(triggerSubmitted);
coordinator.scheduleForBenchmark();
triggerSubmitted.await();After the measured invocation, wait for the mocked ACK and checkpoint completion outside the measured region: fixture.awaitCheckpointCompleted();This prevents one sample from leaking pending checkpoint state into the next sample. ComparisonThe existing workflow compiles and runs each revision separately:
@Vivek1106-04 We look forward to your reply |
|
@nzw921rx @DanielLeens On the question of which direction this harness should take: I Taking the two options as Daniel framed them. Driving the real Two reasons I do not think that answers the question this benchmark was built for. The queuing-faithful harness. @nzw921rx's objection is that the current state arms It also fixes the reading of the current sweep, which is Daniel's other point: the knee So unless you want the full-fidelity version for its own sake, I will:
If you would rather have the real-engine-path version, or both arms, say so and I will Separately, this PR's earlier three items remain addressed at this head: 4g heap, README |
Thank you, please take a look at my design above. |
|
@nzw921rx Our comments crossed - mine went up about three minutes after yours and was You are right and my objection does not apply to your version. I argued against Five things I checked against the code before replying. One of them I think removes work 1. The testing constructor is not needed, and cannot work as written
So a benchmark that constructs a coordinator through the existing public constructor gets It also keeps this PR test-only. Adding a constructor to 2. The interval guard will stop the probe firing after the first sampleThis is the one that needs a decision from you, because it shapes the fixture. long interval = currentTimestamp - latestTriggerTimestamp.get();
if (interval < coordinatorConfig.getCheckpointInterval()) {
scheduleTriggerPendingCheckpoint(
checkpointType, coordinatorConfig.getCheckpointInterval() - interval);
return;
}
Three ways out, and I would like your call:
I lean to the first: slow but honest, and JMH 3. The single-shot probe can be tripped by the previous sample
4. Mockito is not available in
|
|
@Vivek1106-04 Here are my views. If you have any different opinions, you can correct them
this.scheduler = manager.leaseCheckpointScheduler(pipelineId);
|
|
@Vivek1106-04 @Vivek1106-04 I agree with the first option in point 2: use the minimum valid To prevent the measured coordinator's automatically re-armed trigger from leaking into the next invocation, we can expose a small coordinator-level test hook: @VisibleForTesting
void cancelScheduledCheckpointTriggers() {
// Cancel only this coordinator's outstanding trigger tasks.
}This method must not shut down or recreate the scheduler:
The invocation setup can then isolate checkpoint completion, timer cleanup, and the 10 ms interval from the measured sample: @Setup(Level.Invocation)
public void prepareInvocation() throws Exception {
awaitPreviousCheckpointCompleted();
// Prevent the measured coordinator's automatic trigger
// from racing with the next explicit benchmark trigger.
coordinator.cancelScheduledCheckpointTriggers();
// Outside the measured region.
TimeUnit.MILLISECONDS.sleep(11);
// Arm the task-specific probe only after the fixture is quiescent.
probe.arm();
}
The measured region remains: The previous checkpoint completion, automatic-trigger cleanup, and interval wait remain outside the measurement. Background coordinators continue running normally to preserve realistic multi-pipeline scheduling load. |
|
Thanks both — this has converged into a genuinely good design, and I want to engage with the technical substance rather than just referee. On the measurement boundary itself: I'm convinced. @nzw921rx's design ( One thing I need to flag before this goes further: the "add a testing constructor that calls
So @Vivek1106-04's point 1 is right as written: a benchmark that goes through the existing 11-arg constructor gets each revision's real scheduler for free — On the interval guard (Vivek's point 2): pinning On Still open, and I don't think it needs me to resolve it: Vivek's point 5 in his 12:56:17 comment — no-cluster fixture (proxied Nothing here blocks continuing the design conversation, and there's no new commit yet, so there's nothing to re-review on the code side. Once this lands as a diff I'll go through it against this design (measurement boundary, interval pinning, and whichever fixture route you pick) rather than re-litigate the discussion. |
|
@nzw921rx @DanielLeens Before choosing between the two fixtures I built both and ran them, on 1. Measure the real periodic trigger instead of injecting oneInjecting a trigger per sample fights the interval guard in The alternative needs neither. Let the coordinator run its own periodic trigger at its configured interval, and observe it: a sample is the time 2. The no-cluster fixture cannot sustain samples
Faking the tasks entirely is also blocked: the fake network has to return an 3. The mini-cluster does not scale to the sweepWith
P = 500 is out of reach on the benchmark's own CPU budget. 4. What works: a real member, real coordinators, fake tasksOne real SeaTunnel member provides the real checkpoint storage, IMap, monitor service and engine context, so on #12165 the coordinators use the real member-wide scheduler. A real P = 500 as 500 jobs of one pipeline, 10 s interval, starts staggered across one interval, 60 s window:
5. Two things the runs forced on the designPipelines must be spread across jobs. The same 500 pipelines as one job averaged 12 s per checkpoint, and no sample was usable. As 500 jobs it was 5.6 ms. I have not proven the cause; my suspicion is that every completion updates one per-job entry in the checkpoint overview IMap. Either way the benchmark should model many jobs, which is also what "500 active pipelines" in #12122 means in practice. The interval is set from the same constraint: at P = 500, a 1 s interval is 500 real checkpoint completions per second and storage cannot keep up, so the sweep uses a fixed 10 s. Absolute delay numbers must come from CI. On macOS every timed wait fires about 4-5 ms late, and a plain Costs, stated up front
NextUnless you see a problem with this, I will replace the standalone harness in this PR with this fixture: sweep P over 1 / 10 / 100 / 500 as jobs of one pipeline, report scheduler thread count and the scheduling delay distribution, fail the run if kept samples drop below a threshold rather than report a number from the re-arm path, and keep the benchmark in |
|
@Vivek1106-04 Thank you for clarifying. I have a question How will the calculated periodic-trigger delay be reported as a JMH metric? JMH measures the execution time of the Please clarify the exact How is it compatible with the existing reporting of CV, ERROR, and SCORE? |
|
Thanks for actually building and running both fixtures instead of settling this by argument — the P=500 table is the evidence that should decide it, and "real member, real coordinators, fake tasks only at the network boundary" is the right shape: no production code changes on either revision, and the two costs you flagged up front (reflection field renames, the two-message fake protocol) are both acceptable and worth a loud failure check as you said. I don't have anything to add on top of @nzw921rx's open question about how the periodic-trigger delay actually surfaces as a JMH One small ask for whenever you do push it: could the two costs (the two reflected fields, and the fake's assumption about which two messages it needs to answer) get a short comment at their read sites in the benchmark source, not just in this thread? That's the part most likely to silently break on an unrelated engine-internals change later. |
|
@nzw921rx Good question. As I described it, the BoundariesState (
So JMH's timed region runs from
Notes on precision:
Reporting: SCORE and ERROR work unchanged, CV does notWith CV is the gap. For sample modes JMH writes the raw data as Two ways to handle it:
I prefer (a). If you would rather keep the tooling out of this PR, I will use (b) here and do (a) in a follow-up. @DanielLeens Yes. The two reflected fields ( |
|
Thanks @Vivek1106-04, this answers the piece I was waiting on. The boundary you describe -- On the SampleTime-vs-AverageTime call: I'd lean toward (a) too (teach Appreciate you committing to comments at both read sites (the two reflected fields, and the fake's two-message assumption) explaining what engine behavior each depends on and that the fixture fails loudly at setup if it's missing -- that was the main thing I wanted nailed down given how easy this would be to silently break on an unrelated engine-internals change. Once that's pushed alongside the CV fix (or the AverageTime fallback), I'll do the final pass on the fixture. |
|
@Vivek1106-04 Two small follow-ups on the fixture design, nothing blocking:
Otherwise nothing to add beyond my last comment; I'll do the final pass once the fixture is pushed. |
For sample modes JMH writes raw data as rawDataHistogram (fork -> iteration -> [value, count] pairs) instead of rawData. jmh_metrics() only flattened rawData, so any SampleTime benchmark got no samples, a standard deviation of 0 and a reported CV of 0%. Each iteration's histogram is now reduced to its mean, so the samples keep the meaning they have for the other modes: variation between iterations, not the spread of individual invocations. Benchmarks that publish rawData are unchanged.
Replaces the standalone thread-pool harness with a fixture that runs the production checkpoint coordinators, so the benchmark measures whichever checkpoint scheduler the engine on the classpath uses and runs unchanged on every revision being compared. One real member provides checkpoint storage, the IMaps, the monitor service and the engine context. A CheckpointManager subclass creates real coordinators through the existing constructor, one single-pipeline job per coordinator, and fakes only the tasks at sendOperationToMemberNode: it reports READY_START and acknowledges each barrier. IMap persistence is off, since its write-through MapStore stalls checkpoint id allocation for hundreds of milliseconds. Each invocation measures one trigger. @setup(Level.Invocation) returns when the trigger is due; the timed @benchmark spins until the coordinator's pendingCounter goes from 0 to 1. The score is the scheduling delay plus the trigger's decision logic up to creating the pending checkpoint. A fixed subset of coordinators, started at least 100 ms apart, is measured; the rest are load. Measuring all of them would drop whichever trigger came due while another was being measured, which biases the result towards short delays under contention. Skips are counted by cause and printed every iteration, and an iteration with more than 10% skipped fails the run. The interval is pipelineNum * 20 ms, floored at 200 ms, so every point of the sweep sees the same trigger rate. Two engine fields are read by reflection: CheckpointCoordinator.pendingCounter and CheckpointBarrierTriggerOperation .barrier. The fixture fails at setup, naming the field, if either is missing.
|
The fixture is pushed (
@nzw921rx The boundaries are the ones in my last comment: the untimed @DanielLeens Both reflected fields and both faked task messages have a comment at their read site. A missing field fails at setup with the class and field named, which Three things the runs changed, compared with what I described earlier:
Local sweep, @nzw921rx @DanielLeens @SEZ9 ready for the final pass. |
… iterations The CI benchmark job smoke-runs every benchmark with one 1-second iteration and no warmup. At 10 pipelines that is about 7 due triggers, so a single skipped trigger was already above the 10% limit, failed the iteration and, with -foe, stopped the benchmarks queued after it. The limit now applies once an iteration has at least 50 due triggers, which a configured 10-second iteration reaches at about 10 samples per second. Shorter iterations still print their skip counts.
DanielLeens
left a comment
There was a problem hiding this comment.
Thanks for the very thorough rework, @Vivek1106-04, and thanks to @nzw921rx and @SEZ9 for pushing the design as hard as you did in the thread — that back-and-forth clearly made this a better benchmark. I re-read the entire discussion plus the currently pushed diff from scratch (not just the summary in the thread) before writing this, and I want to independently confirm the parts I can verify from source rather than just agree by default.
What Problem Does This PR Solve?
- User pain point: there was no way to measure how late a periodic checkpoint trigger runs relative to when it is due, on the real scheduling path.
CheckpointingTimeBenchmarkreflects straight intocreatePendingCheckpoint/startTriggerPendingCheckpointand is dominated by the barrier round-trip, so it is close to blind to the scheduling layer itself — which is exactly the layer #12339/#12165 want to change. - Fix approach (current head, materially different from the version I reviewed on 09-21):
CheckpointSchedulingBenchmarknow runspipelineNumrealCheckpointCoordinators inside one real SeaTunnel member (CheckpointSchedulingFixture), with tasks faked only at the network boundary (FakeTaskCheckpointManager#sendOperationToMemberNode). A@Setup(Level.Invocation)picks the coordinator due soonest and parks/spins to its due time (untimed); the@Benchmarkbody spins until that coordinator'spendingCounterflips 0→1 (timed). Only everyprobeStride-th coordinator is measured; the rest are real load. - One-sentence summary: the benchmark now measures scheduling delay on the actual production trigger path (
tryTriggerPendingCheckpoint, its interval/min-pause guards, the real per-pipeline or shared scheduler) instead of on a standalone simulated thread pool.
Before/after in one example: at pipelineNum=500 on dev, the CI run on this head logs checkpoint scheduler threads for 500 pipelines: 996 — i.e. it is actually observing ~2 threads per real CheckpointCoordinator.scheduler (Executors.newScheduledThreadPool(2, ...), CheckpointCoordinator.java:239-250 on dev), not a synthetic stand-in pool.
1. Code Change Review
1.1 Core Logic Analysis
Files (relative to dev, 8 changed, +1137/-1): CheckpointSchedulingBenchmark.java, CheckpointSchedulingFixture.java (new, 578 lines), FakeTaskCheckpointManager.java (new), BenchmarkReflection.java (new), CheckpointSchedulingBenchmarkTest.java (rewritten), tools/benchmarks/save_jmh_result.py, tools/benchmarks/test_save_jmh_result.py, .github/workflows/benchmarks.yml (unchanged from the earlier round).
I verified the following claims against source rather than taking them on faith:
sendOperationToMemberNodeis genuinely the seam production code uses for both task-start and barrier dispatch.CheckpointCoordinator.allTaskReady()callsnotifyTaskStart()→checkpointManager::sendOperationToMemberNodeforNotifyTaskStartOperation, andjoin()s the futures before arming the first trigger (CheckpointCoordinator.java:469-479);triggerCheckpoint()sendsCheckpointBarrierTriggerOperationthe same way (CheckpointCoordinator.java:1175-1178).FakeTaskCheckpointManageronly special-cases the barrier operation and answers everything else (including the start notification) with a completed no-op future — that is sufficient becauseallTaskReady()only waits for the futures to complete, not for a specific payload.- The two reflected field names are correct on
dev.CheckpointCoordinator.java:169:private final AtomicInteger pendingCounter = new AtomicInteger(0).CheckpointBarrierTriggerOperation.java:protected Barrier barrier, populated from the realCheckpointBarrierpassed bytriggerCheckpoint(CheckpointBarrier)(CheckpointCoordinator.java:1175-1177), so the(CheckpointBarrier) BARRIER_FIELD.get(operation)cast inFakeTaskCheckpointManager.readBarrieris sound. Both reads are behindBenchmarkReflection.requireField, which fails at class-load with the class+field name if either is renamed — good, matches what was promised in the thread. - No production/engine code changed. The whole diff outside
seatunnel-benchmarks/andtools/benchmarks/is the one-linebenchmarks.ymlregistration. This matches the explicit decision in the thread to keep this PR test-only rather than adding a testing constructor or a cancellation hook toCheckpointCoordinator. - This closes my own Issue 1 from the 09-21 review ("the per-pipeline baseline cannot show queueing, so the sweep parameters barely move the sample") — that harness is gone. The new fixture measures the real
tryTriggerPendingCheckpointdecision logic (interval guard atCheckpointCoordinator.java:782-799, the lock, thependingCounterre-arm check) under real multi-pipeline load. I'd consider this the direct, verified resolution of @nzw921rx's CHANGES_REQUESTED as well (see Merge Recommendation). - I checked the fork's CI run for this head (
Vivek1106-04/seatunnelrun35957406479, jobbenchmark-test (8, ubuntu-latest)), not just the description's numbers:CheckpointSchedulingBenchmarkTestpasses 7/7, and the actual sweep log lines are consistent with what the PR description claims, e.g.# checkpoint scheduler threads for 500 pipelines: 996; measuring 100 of them/measured 10 of 11 due triggers. So the "it actually works end-to-end" claim isn't just asserted, it's observable in this head's own CI output.
Runtime path (engine-touching, so per the review requirements I'm tracing it explicitly): FakeTaskCheckpointManager.startTask() → CheckpointManager.reportedTask(READY_START) → CheckpointCoordinator.reportedTask → allTaskReady() → notifyTaskStart()/scheduleTriggerPendingCheckpoint(interval) → real scheduler.schedule(...) → tryTriggerPendingCheckpoint() → createPendingCheckpoint → pendingCounter.incrementAndGet() (observed by the fixture) → triggerCheckpoint() → sendOperationToMemberNode(CheckpointBarrierTriggerOperation) → fake ack → checkpoint completes → pendingCounter back to 0 → next schedule(..., interval). This is the normal production path end to end; nothing here is a recovery/exception/race-only path.
1.2 Compatibility Impact
Fully compatible. Benchmark-only code (seatunnel-benchmarks) plus a benchmark-tooling change plus one workflow_dispatch choice entry. No config option, default, protocol, serialization, checkpoint/savepoint format, or SPI is touched; no existing benchmark's behavior changes for benchmarks that still publish rawData (see 1.3).
1.3 Performance / Side-Effect Analysis
- The fixture starts a real Hazelcast member per JMH trial with up to ~1000 real threads at
pipelineNum=500— heavy, but that's the point of the benchmark and it's gated behind-Pbenchmark/benchmark-test, never the defaultunit-testjob. CI completed all four points of the sweep in well under a minute onubuntu-latestwith the reduced CI iteration count. IMappersistence is explicitly disabled inSchedulingEnvironmentContext(CheckpointSchedulingFixture.java, theSchedulingEnvironmentContextinner class) to avoid the write-throughMapStorestalling checkpoint-id allocation — a deliberate, documented, and narrowly-scoped override (only theengine*map config, onlyMapStoreConfig.setEnabled(false)), not a blanket change to the storage benchmark's shared environment class.save_jmh_result.py:histogram_mean/iteration_scoresonly change behavior whenrawDatais absent fromprimaryMetric(i.e., only forSampleTime-mode benchmarks, which today is only this one). Every existingAverageTime/Throughputbenchmark keeps hitting the originalflatten(primary.get("rawData", []))path unchanged — I traced this explicitly since it's shared tooling and a regression here would silently corrupt CV for every other benchmark's dashboard history.
1.4 Error Handling and Logging
No swallowed exceptions on the main paths: checkFailure() propagates coordinator errors recorded via FakeTaskCheckpointManager.handleCheckpointError (AtomicReference<Throwable>, compareAndSet) into the benchmark thread instead of silently continuing; awaitTrigger() and resync() both fail loudly with a descriptive IllegalStateException on a stuck due trigger; endIteration() fails the iteration outright (not just logs) when skips exceed 10% of due triggers, per MAX_SKIP_RATIO. This is exactly the "fail the sample loudly rather than report a wrong number" design the thread converged on, and it's implemented as described.
Issue 1: The printed scheduler-thread count can under-report because resync() at setUp() only waits for probes, not every coordinator
- Location:
CheckpointSchedulingBenchmark.java:125-131(theSystem.out.printfright afterfixture.setUp()),CheckpointSchedulingFixture.java:395-421(resync()only iteratesi += probeStride),CheckpointSchedulingFixture.java:345-349(countSchedulerThreads()). - Problem:
resync()guarantees every probe coordinator has triggered at least once (so its pool has started its second worker), but non-probe ("load") coordinators are never waited on.ScheduledThreadPoolExecutorstarts its second worker only after a periodic task has run once, so a load coordinator that hasn't triggered yet still shows one thread, not two. The printed count is therefore a lower bound, not "2P", at the momentsetUp()returns — this is the same class of nondeterminism as Issue 3 in my 09-21 review, just re-surfacing in the new design. It's visible in this head's own CI log:10 pipelines: 15(not ~20),100 pipelines: 196,500 pipelines: 996(both short of 200/1000). - Potential risk: low — this line is purely informational/logging, it does not feed the measured sample or the skip-ratio guard, so it cannot corrupt a reported delay number. But the class Javadoc/log line don't say "at least" or "a lower bound", so a reader comparing this number against #12165's thread count could slightly overstate the
dev-side thread cost. - Best improvement: either wait for every coordinator (not just probes) to complete one trigger before printing (cheap: an extra
resync-style pass over all indices, done once atsetUp()), or just change the log text to "checkpoint scheduler threads for %d pipelines (lower bound, load coordinators may not have started their second worker yet): %d". - Severity: Low (non-blocking)
- Raised by another reviewer: No (evolution of my own 09-21 Issue 3 against the new implementation)
Issue 2: shouldStopEverySchedulerThreadOnTearDown still does a JVM-wide thread-name scan
- Location:
CheckpointSchedulingBenchmarkTest.java:79-99(test body, thecountSchedulerThreads() > 0poll loop and the finalassertEquals(0L, ...)),CheckpointSchedulingFixture.java:71(SCHEDULER_THREAD_NAME_PREFIX = "checkpoint-", now broader than the old"checkpoint-coordinator-"). - Problem: this is the same shape of assertion I flagged as Issue 2 on 09-21 (still unaddressed in that specific respect, even though the harness around it was rewritten):
Thread.getAllStackTraces()is scanned process-wide.CheckpointingTimeBenchmarkTestin the same module/surefire JVM also starts a real member whose realCheckpointCoordinators use the same"checkpoint-coordinator-*"-style prefix. If any of those threads from a different test class is still winding down when this assertion runs, the test can fail through no fault of this class — and the widened prefix ("checkpoint-"vs the old"checkpoint-coordinator-") makes it match more thread names, not fewer. - Potential risk: low-probability flake in the
benchmark-testjob, same as before; I could not reproduce it and CI has been green on this exact assertion across the pushed commits, so I'm not treating it as a regression, just an unresolved carry-over. - Best improvement: unchanged from my earlier suggestion — have the fixture's coordinator/member thread factories record the threads they create and
join(timeout)/!isAlive()on those specifically, instead of a global name-prefix scan. - Severity: Low (non-blocking, carried over)
- Raised by another reviewer: No
Issue 3 (nit): two same-named constants with different values in coupled classes
- Location:
CheckpointSchedulingBenchmark.java:88(MIN_CHECKPOINT_INTERVAL_MILLIS = 200L, the benchmark's chosen floor forpipelineNum * triggerSpacingMillis) vs.CheckpointSchedulingFixture.java:83(MIN_CHECKPOINT_INTERVAL_MILLIS = CheckpointConfig.MINIMAL_CHECKPOINT_TIME, i.e.10L, the engine's hard floor enforced invalidateParameters()). - Problem: both are
static final long MIN_CHECKPOINT_INTERVAL_MILLISwith different values and different meanings (a benchmark design choice vs. an engine-enforced minimum). Not a bug today — each class correctly uses its own constant — but the identical name across two directly-coupled classes is an easy trap for a future edit. - Best improvement: rename the benchmark-side one, e.g.
TRIGGER_SPACING_FLOOR_MILLIS, to make the two constants visibly distinct. - Severity: Low (nit, non-blocking)
- Raised by another reviewer: No
2. Code Quality Assessment
2.1 Coding Standards
Good. ASF headers on all four new files, no wildcard imports, comments consistently explain why (the early-expected offset at the spin loop, why only probes are measured, why IMap persistence is off, why the reflected fields are safe) rather than restating the code. BenchmarkReflection is a small, well-scoped, single-purpose utility. One Medium-ish documentation gap, not a coding-standards violation: neither the class Javadoc nor the log line in Issue 1 above documents that the printed thread count can under-report for load coordinators — worth a one-line addition.
2.2 Test Coverage and Test Stability
Coverage is appropriate for a benchmark harness: CheckpointSchedulingBenchmarkTest now exercises the real fixture end-to-end (shouldMeasureDueTriggersOfRealCoordinators, 20 real samples against real coordinators), lifecycle/teardown, parameter validation (verified to fail before any real member starts, since validateParameters() is the first statement in setUp() — no resource leak on the negative-parameter path), the pure probeStride/isSkipShareTooHigh helpers, and the reflection-failure message. This is a real improvement over the 09-21 version's harness-only tests, since it now exercises the actual engine path rather than a simulated stand-in.
Mandatory flaky-test stability analysis — rating: Risk present.
- Hard sleeps: none of the concerning kind.
startStaggered/awaitNextDueTrigger/resyncuseLockSupport.parkNanosto a computed deadline plus a bounded busy-spin, not a fixedThread.sleepguess;resync()'s deadline scales with the configured interval (RESYNC_INTERVALS * intervalNanos + PENDING_REARM_NANOS), so it isn't a magic-number timeout independent of the test's own parameters. - Nondeterministic assertions: none found —
shouldMeasureDueTriggersOfRealCoordinatorsasserts an exact sample count (SAMPLE_COUNT) reached through a loop that retries on skips rather than asserting on wall-clock thresholds. - Shared static state: none across fixture instances;
BenchmarkReflection's cachedFields are read-only reflection handles, not mutable shared state. - Order dependence / resource release:
shouldMeasureDueTriggersOfRealCoordinatorsreleases viafinally; the parameter-validation test never allocates a resource to begin with (see above).shouldStopEverySchedulerThreadOnTearDowndoesn't wrap its ownsetUp()/tearDown()pair in try/finally, but that test's whole point is to calltearDown()itself, so this isn't a leak risk in the same way — only Issue 2's shared-prefix scan is a genuine (low-probability) flake vector. - Weak readiness signals: the opposite —
resync()explicitly waits for "seen idle, then seen triggering" per probe rather than assuming readiness after a fixed delay, which is a stronger signal than most E2E readiness checks in this codebase. - Where it runs: only
benchmark-test (8|11, ubuntu-latest), gated behind-Pbenchmark; confirmed green on this exact head (7/7 inCheckpointSchedulingBenchmarkTest,Tests run: 7, Failures: 0, Errors: 0at04:57:04Z–04:57:07Zin the fork CI log). - Net: I'm not rating this Stable outright because Issue 2's cross-test-class thread-name scan is a real, if low-probability, flake mechanism that predates this round and remains unaddressed; nothing here rises to High risk.
test_save_jmh_result.py's new case (test_uses_iteration_means_of_sample_time_histograms) has real failure capability: it asserts exact computed values ([15.0, 20.0, 25.0], sample_standard_deviation == 5.0) from a two-fork, mixed-bucket, one-empty-iteration histogram fixture, so a wrong weighting, a wrong iteration boundary, or a broken empty-iteration skip would all fail it, not just a presence check. The PR description states this test failed before the fix, which is exactly the "delete-the-implementation and watch it fail" property this needs.
2.3 Documentation Updates
No user-facing behavior changed, so docs/en/docs/zh don't need updates, and I agree with leaving seatunnel-benchmarks/README.md and docs/en/engines/zeta/benchmark.md untouched (neither enumerates individual benchmark classes). Minor: the PR description's "How was this patch tested?" section still says Tests run: 6, but the class now has 7 @Test methods and CI confirms Tests run: 7 — please update the description before merge; purely cosmetic, not a code issue.
3. Architectural Soundness
3.1 Elegance of the Solution
This is now the "faithful" option (Option B) from my 09-21 review rather than the "documented control" option (Option A), and I think that was the right call given #12165 needs a real comparison, not just a control. Real member + real coordinators + fakes only at the task-network boundary is a clean seam: it reuses an existing, already-battle-tested pattern (SeaTunnelStorageEnvironmentContext/StorageLifecycleFixtureJob/CheckpointStorageBenchmarkFixture already do "real member, fake edges" for other benchmarks in this same module) rather than inventing a new fixture style.
3.2 Maintainability
Good, with the caveats above. The two reflection points and the two faked task messages each have a comment at their read/answer site explaining what engine behavior they depend on, as promised in the thread, and a missing field now fails loudly at class-load with the class+field name (shouldNameTheEngineFieldWhenItNoLongerExists covers this). com.hazelcast.spi.impl.operationservice.impl.InvocationFuture is an internal Hazelcast type, but that's inherited from the CheckpointManager#sendOperationToMemberNode signature being overridden, not new fragility introduced by this PR.
3.3 Extensibility
Same fixture (real member + fakes at the boundary) runs unchanged against dev's per-pipeline scheduler and #12165's shared scheduler, which is the whole point — no forked benchmark sources, confirmed by the PR description's side-by-side dev vs. #12165 thread-count/skip table using "the same benchmark code on both."
3.4 Historical-Version Compatibility
Not affected. No config, wire format, checkpoint/savepoint format, or upgrade path is touched; this is test-only code with zero production surface change.
4. Issue Summary
| # | Issue | Location | Severity |
|---|---|---|---|
| 1 | Printed scheduler-thread count can under-report (resync only covers probes, not load coordinators) | CheckpointSchedulingBenchmark.java:125-131, CheckpointSchedulingFixture.java:395-421,345-349 |
Low |
| 2 | Teardown test still does a JVM-wide thread-name scan (carried over from 09-21 Issue 2) | CheckpointSchedulingBenchmarkTest.java:79-99, CheckpointSchedulingFixture.java:71 |
Low |
| 3 | Two same-named MIN_CHECKPOINT_INTERVAL_MILLIS constants with different meanings in coupled classes |
CheckpointSchedulingBenchmark.java:88, CheckpointSchedulingFixture.java:83 |
Low |
No Medium or High-severity issues. CI status on this head: Vivek1106-04/seatunnel fork run 35957406479 — benchmark-test (8, ubuntu-latest) and (11, ubuntu-latest) both green, benchmark-tools-test (covers the Python change) green, License header/Code style green.
5. Merge Recommendation
Conclusion: Ready to merge
- Blockers — must be fixed: none.
- Recommended fixes — non-blocking, fine to take in a follow-up or a quick push either way:
- Issue 1: note "lower bound" in the log line, or resync all coordinators (not just probes) before printing.
- Issue 2: track fixture-owned threads directly instead of a global name-prefix scan, to remove the remaining cross-test-class flake vector.
- Issue 3: rename the benchmark-side
MIN_CHECKPOINT_INTERVAL_MILLISfor clarity. - Cosmetic: PR description's test count (6 → 7).
- Overall assessment: this is now a materially different and substantially stronger implementation than the one I reviewed on 09-21. It measures the real
CheckpointCoordinator/tryTriggerPendingCheckpointpath under real multi-pipeline load, with no production code touched, a documented and bounded skip-guard instead of silently reporting bad samples, and CI evidence (not just claims) that it actually completes real checkpoints end to end atpipelineNumup to 500. The shared-tooling change tosave_jmh_result.pyis scoped correctly and is itself tested with a fixture that has genuine failure capability.
Response to the existing CHANGES_REQUESTED from @nzw921rx: your original point — "this benchmark only simulates a single pipeline occupying two threads... it does not exercise the current engine execution path" — was correct about the version it was written against, and I'd treat it as the same underlying concern as my own Issue 1 from that round. Based on what I verified above (real CheckpointCoordinators, real tryTriggerPendingCheckpoint, fakes only at sendOperationToMemberNode, confirmed completing real checkpoints on this head's own CI run), I believe this is now resolved at the source level, and the design you and Vivek converged on in the thread is exactly what got pushed. I'd suggest re-reviewing the current head when you have a chance to confirm you agree it's resolved, since your review state is what's currently blocking the merge gate — but from my independent read of the source and CI, I don't see a remaining engine-fidelity gap.
Nice work getting this to a "real path, no engine changes, CI-verified" state, @Vivek1106-04 — and thanks again for the patience through two full design iterations.
- Trial setup now resyncs every coordinator, not only the probes, so every scheduler is armed before the thread count is printed; the printed count was a lower bound before. - The fixture records checkpoint-* threads that exist before its setup and counts only newer ones, so a thread left by another test class in the same JVM cannot fail the teardown test. - Rename the benchmark-side interval floor to MIN_MEASURABLE_INTERVAL_MILLIS, distinct from the fixture's MIN_CHECKPOINT_INTERVAL_MILLIS.
|
@DanielLeens Thank you for the approval and for checking the source and CI yourself instead of relying on my summary. I have pushed all three of your low-severity items in
The test count in the description is corrected as well. @nzw921rx When you have time, could you look at the current head again? Your concern that the benchmark did not exercise the real engine path shaped the rewrite. The benchmark now runs real Thanks to @nzw921rx, @SEZ9 and @DanielLeens for reviewing this so carefully over several rounds. The benchmark is much stronger because of your feedback than the first version I opened. |
DanielLeens
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround, @Vivek1106-04. I re-reviewed the latest head at 550c22568, re-reading the entire diff against dev from scratch rather than just this last commit, and re-checked the production call chain and thread-naming assumptions against the current source again rather than reusing my notes from the 09-24 approval.
All three low-severity items from my previous review are resolved, cleanly and precisely:
- Printed scheduler-thread count was a lower bound.
setUp()now resyncs every coordinator, not just the probes, before printing (resync(1)), so the line reflects the full thread cost atpipelineNumpipelines instead of only what the probes had armed so far. - Teardown test scanned every thread in the JVM. The fixture now snapshots
checkpoint-*threads that already exist before its ownsetUp()starts and only counts threads that appear after that. The rewrittenshouldStopEverySchedulerThreadOnTearDowntest proves this directly: it starts a syntheticcheckpoint-foreignthread before the fixture, and the fixture's own thread count still reaches exactly 0 while that thread stays alive. I checked that reverting this filter would make the test fail (the foreign thread would be miscounted), so this is a real regression test for the bug it fixes, not a rename. - Two same-named constants with different meanings. The benchmark's measuring-floor constant is renamed to
MIN_MEASURABLE_INTERVAL_MILLIS, with a Javadoc that now explains how it differs from the fixture's engine-enforcedMIN_CHECKPOINT_INTERVAL_MILLIS.
I didn't find anything new. The two files this round's commit doesn't touch (FakeTaskCheckpointManager, BenchmarkReflection) I re-checked against current dev anyway as part of the full re-review, and my 09-24 verification of the production seams (CheckpointManager#sendOperationToMemberNode, reportedTask, the reflected pendingCounter/barrier fields) still holds. save_jmh_result.py is unchanged and I re-confirmed its rawData-present fast path is untouched for every other benchmark.
Test stability: this round's test changes get a Stable rating from me. No hard sleeps, no nondeterministic assertions, and the new test actually removes a piece of implicit shared state (the global thread namespace) rather than adding any. I fetched the fork's own CI log for this exact head (not just the checks summary) and confirmed CheckpointSchedulingBenchmarkTest: 7/7 passing.
One thing worth calling out for whoever merges this: GitHub's review decision on this PR is still CHANGES_REQUESTED from @nzw921rx, recorded against commit 556de1f9227 — the version before the fixture was rewritten specifically to answer his concern that the benchmark didn't exercise the real engine path. I independently verified the current fixture does exercise the real CheckpointCoordinator/tryTriggerPendingCheckpoint path in my prior review and again this round. @Vivek1106-04 already pinged @nzw921rx for a fresh look on the current head — from my side there's no remaining technical blocker, just that outstanding human review to refresh.
Nice work closing these out quickly and precisely, @Vivek1106-04 — no scope creep, and a test that actually defends the fix.
Conclusion: Ready to merge
- Blockers — must be fixed: none.
- Recommended fixes — non-blocking: none remaining.
- Overall: this is a small, well-targeted fix for exactly the three points raised, backed by a genuine regression test and green CI on the current head. The only open item is procedural — please get @nzw921rx's re-review on
550c22568before merging, since that's what the currentCHANGES_REQUESTEDstate is gating on.
| @Warmup(iterations = 2, time = 10) | ||
| @Measurement(iterations = 5, time = 10) | ||
| @Fork( | ||
| value = 2, |
There was a problem hiding this comment.
Or can you explain what considerations fork=2 is based on?
There was a problem hiding this comment.
Changed to 3, so it matches BenchmarkBase and every other benchmark in the module. For this benchmark the extra fork is useful in itself: JIT state, thread placement and timer behaviour vary between JVMs, and scheduling delay is sensitive to all three, so more forks give a more representative distribution and a more stable CV. The cost is about 5 more minutes for the full pipelineNum sweep (4 points x 7 iterations x 10 s per fork, plus member startup).
|
Thanks @Vivek1106-04 —
Nothing further from me on these. I see @nzw921rx has left a couple of new comments on the current head. I'll leave those between the two of you — please respond on the thread (or push a follow-up if you agree) so we have a clear final state before this is merged. |
…kpoint package Run the scheduling benchmark with 3 forks, like the other benchmarks.
|
@nzw921rx Agreed, moved The fork count is now 3 as well; details in the inline thread. |
nzw921rx
left a comment
There was a problem hiding this comment.
+1
Non-blocking suggestion: In the future, consider increasing the warmup iterations (@WarmUp(iterations)) from 2 to 3 to ensure sufficient JIT compilation, avoid measurement being affected by compilation overhead during the formal measurement phase, and thereby improve the stability of benchmark results.
|
Hi @Vivek1106-04, following up after the merge, I ran the benchmark on Linux: https://github.com/apache/seatunnel/actions/runs/36820452487. The run already uses three 10-second warmup iterations, but some measurements still trend downward. For example, with one pipeline on JDK 8, the first fork’s measurement scores are 161.505, 163.602, 143.061, 142.223 and 131.410 μs/op. Could you please investigate whether warmup is sufficient and what causes this trend? Please also run the complete benchmark sweep locally, including all parameter combinations and forks, and share the commands, environment details and full per-iteration results. As a follow-up, we should also consider a fixed-checkpoint-interval scenario and longer measurement runs to evaluate increasing scheduling load and obtain enough samples for tail percentiles. This would help strengthen the baseline for evaluating the shared scheduler. |
|
@nzw921rx Thanks for running it, and sorry for the slow reply. I'm on it now and will start the local runs within the next hour. A first look at the per-iteration data from run 36820452487: only the Plan:
I'll take the fixed-interval scenario and longer runs for tail percentiles as a separate follow-up. |
|
@nzw921rx Thanks for running it on Linux. You were right: the warmup is not sufficient. Below are the cause, the evidence, a proposed fix, and the full sweep you asked for. I have not opened a PR for the fix yet; there is a question for you at the end. What the score containsThe measured window runs from the time a trigger is due until its body has incremented
Both parts fall for the first two minutes. "Due -> body start" is not only the OS wake-up: it also covers Why it trends: the JIT compiles by invocation countThe trigger path runs 5 to 50 times a second, so it is compiled by how often it ran, not by how long the fork has run. From
A causal check: with The one-pipeline point is hit hardest because of the 200 ms interval floor. The class Javadoc says every point sees the same trigger rate, but at one pipeline the member triggers 5 checkpoints a second where every other point triggers 50. Its code warms up ten times slower. Even once settled, its body runs at a lower tier (about 21 us against 6.6 us at ten pipelines), and its trigger wakes up on an otherwise idle 4-vCPU VM, which costs about 25 us more (about 95 against 70 us). That is why one pipeline scores higher than ten. Why CV shrinks as pipelines grow: three separate effectsDecomposing your run's CV (computed, like the report, from the 15 iteration means):
So CV is not monotonic in the pipeline count; the one-pipeline point is worst because it has the drift, 50 samples per iteration instead of 100, and the noisiest wake-ups. Proposed fixOn branch
A first attempt, warming up on throwaway coordinators and cancelling them, did not work: cancelling them made the JIT deoptimise Full sweep,
|
| pipelineNum | mean trend per iteration, dev | fix | forks falling > 2% per iteration, dev | fix | CV, dev | fix | score range (us), dev | fix |
|---|---|---|---|---|---|---|---|---|
| 1 | -3.5% | -0.3% | 8 / 12 | 5 / 12, scattered both ways | 10.5% | 9.4% | 166-200 | 94-130 |
| 10 | -1.9% | +1.0% | 3 / 12 | 0 / 12 | 7.0% | 3.9% | 87-105 | 77-95 |
| 100 | -1.5% | +0.3% | 3 / 12 | 0 / 12 | 4.5% | 3.6% | 82-103 | 73-90 |
| 500 | -1.0% | -0.2% | 2 / 12 | 1 / 12 | 4.5% | 4.3% | 100-121 | 93-111 |
No run of the fix hit the skip limit; one earlier dev run did, at 100 pipelines on JDK 11 (19 triggers came due while another was being measured).
What remains, and is not a warmup effect: at one pipeline, iteration-to-iteration variation stays around 9 to 10% on both sides, because that trigger's wake-up latency on an idle VM wanders by tens of microseconds over minutes. Forks still settle 1 to 5% apart. The per-iteration median is much steadier than the mean (CV 2 to 4% at ten pipelines or more with the fix), because the mean carries the occasional millisecond sample; reporting it would be a separate change to save_jmh_result.py.
Commands and environment
# Build, from each checkout
./mvnw -B -q -Pbenchmark -pl seatunnel-benchmarks -am -DskipTests -Dskip.spotless=true package
# Run, same as tools/benchmarks/run_benchmarks.sh
java -jar seatunnel-benchmarks/target/benchmarks.jar CheckpointSchedulingBenchmark -f 3 -wi 3 -i 5 -foe true -rf json -rff <side>.json
- GitHub-hosted
ubuntu-24.04, Linux 6.17.0-1022-azure x86_64, AMD EPYC 7763, 4 vCPU - Temurin 11.0.32.1 and 17.0.20.1. JDK 8 is no longer in the comparison because
devnow builds for Java 11, andbenchmarks.ymlruns 11 and 17. dev=b3898b62d; fix =738f372e4on the throwaway verification branch, identical benchmark code to1914eabf6- The fork JVM options are the benchmark's own
@Forksettings.
These ran on GitHub's runners in my fork rather than on my machine: on macOS every timed wait fires about 5 ms late, and Docker Desktop's Linux VM inherits that (one pipeline measured about 2.5 ms there against about 150 us on the runners), so a local run cannot show a microsecond-scale trend.
Per-iteration means (us), all forks
JDK 11, runner job j11-dev-then-fix (ran dev then fix)
| side | pipelineNum | fork | iter 1 | iter 2 | iter 3 | iter 4 | iter 5 |
|---|---|---|---|---|---|---|---|
| dev | 1 | 1 | 205.7 | 210.8 | 187.5 | 184.6 | 191.7 |
| dev | 1 | 2 | 202.7 | 169.8 | 175.9 | 169.8 | 166.1 |
| dev | 1 | 3 | 180.0 | 157.1 | 152.8 | 169.7 | 153.7 |
| dev | 10 | 1 | 91.4 | 89.4 | 91.0 | 90.3 | 92.6 |
| dev | 10 | 2 | 99.3 | 98.2 | 99.2 | 95.1 | 97.2 |
| dev | 10 | 3 | 121.3 | 99.4 | 102.4 | 99.4 | 97.5 |
| dev | 100 | 1 | 111.9 | 107.7 | 106.0 | 98.1 | 95.8 |
| dev | 100 | 2 | 115.2 | 97.5 | 111.1 | 97.0 | 95.7 |
| dev | 100 | 3 | 101.7 | 95.1 | 96.1 | 99.1 | 102.5 |
| dev | 500 | 1 | 123.0 | 114.3 | 116.9 | 117.0 | 115.4 |
| dev | 500 | 2 | 143.1 | 106.7 | 106.3 | 106.7 | 110.9 |
| dev | 500 | 3 | 128.1 | 133.2 | 139.4 | 128.1 | 120.7 |
| fix | 1 | 1 | 117.7 | 121.7 | 139.0 | 109.4 | 108.8 |
| fix | 1 | 2 | 109.2 | 106.5 | 130.2 | 104.9 | 96.0 |
| fix | 1 | 3 | 107.1 | 131.2 | 132.6 | 126.2 | 104.4 |
| fix | 10 | 1 | 85.9 | 85.1 | 88.2 | 87.5 | 88.2 |
| fix | 10 | 2 | 88.1 | 87.4 | 88.1 | 88.1 | 88.9 |
| fix | 10 | 3 | 87.7 | 87.6 | 89.7 | 91.1 | 114.7 |
| fix | 100 | 1 | 86.1 | 87.5 | 88.4 | 86.0 | 82.7 |
| fix | 100 | 2 | 86.0 | 83.7 | 85.5 | 84.3 | 98.5 |
| fix | 100 | 3 | 84.4 | 83.2 | 94.8 | 85.5 | 84.8 |
| fix | 500 | 1 | 97.8 | 99.1 | 99.1 | 98.0 | 98.7 |
| fix | 500 | 2 | 99.7 | 104.3 | 102.1 | 101.2 | 105.2 |
| fix | 500 | 3 | 98.9 | 101.1 | 101.5 | 103.3 | 99.6 |
JDK 11, runner job j11-fix-then-dev (ran fix then dev)
| side | pipelineNum | fork | iter 1 | iter 2 | iter 3 | iter 4 | iter 5 |
|---|---|---|---|---|---|---|---|
| dev | 1 | 1 | 239.7 | 226.5 | 188.2 | 168.1 | 179.8 |
| dev | 1 | 2 | 199.6 | 190.7 | 195.3 | 178.2 | 206.0 |
| dev | 1 | 3 | 226.4 | 215.8 | 221.1 | 183.8 | 179.1 |
| dev | 10 | 1 | 104.6 | 102.2 | 101.4 | 98.1 | 99.3 |
| dev | 10 | 2 | 102.7 | 104.4 | 103.4 | 102.9 | 99.5 |
| dev | 10 | 3 | 103.8 | 112.0 | 140.4 | 100.7 | 101.7 |
| dev | 100 | 1 | 98.5 | 118.1 | 99.1 | 100.6 | 100.8 |
| dev | 100 | 2 | 108.7 | 110.5 | 115.3 | 96.7 | 103.7 |
| dev | 100 | 3 | 100.2 | 98.6 | 95.6 | 100.7 | 99.6 |
| dev | 500 | 1 | 124.6 | 120.4 | 111.7 | 115.3 | 121.3 |
| dev | 500 | 2 | 118.3 | 117.7 | 115.8 | 116.1 | 121.0 |
| dev | 500 | 3 | 123.3 | 120.4 | 113.4 | 112.8 | 114.8 |
| fix | 1 | 1 | 121.1 | 142.2 | 137.7 | 130.3 | 145.2 |
| fix | 1 | 2 | 127.9 | 120.1 | 124.0 | 115.5 | 114.6 |
| fix | 1 | 3 | 135.7 | 146.9 | 153.1 | 126.8 | 111.5 |
| fix | 10 | 1 | 97.0 | 94.3 | 93.6 | 93.8 | 95.8 |
| fix | 10 | 2 | 93.1 | 90.8 | 100.4 | 96.4 | 95.7 |
| fix | 10 | 3 | 98.8 | 94.6 | 94.0 | 95.2 | 94.1 |
| fix | 100 | 1 | 90.3 | 89.5 | 95.5 | 91.5 | 88.8 |
| fix | 100 | 2 | 91.8 | 93.4 | 89.8 | 87.9 | 87.0 |
| fix | 100 | 3 | 90.1 | 90.2 | 91.7 | 89.4 | 87.4 |
| fix | 500 | 1 | 109.5 | 110.5 | 113.4 | 111.4 | 110.9 |
| fix | 500 | 2 | 106.5 | 109.7 | 115.2 | 110.0 | 107.4 |
| fix | 500 | 3 | 121.3 | 108.0 | 107.8 | 113.6 | 112.0 |
JDK 17, runner job j17-dev-then-fix (ran dev then fix)
| side | pipelineNum | fork | iter 1 | iter 2 | iter 3 | iter 4 | iter 5 |
|---|---|---|---|---|---|---|---|
| dev | 1 | 1 | 154.2 | 154.7 | 159.1 | 165.1 | 139.7 |
| dev | 1 | 2 | 170.6 | 173.2 | 182.8 | 162.6 | 169.1 |
| dev | 1 | 3 | 191.2 | 227.1 | 171.8 | 154.7 | 181.4 |
| dev | 10 | 1 | 104.3 | 102.3 | 102.1 | 93.9 | 107.0 |
| dev | 10 | 2 | 120.1 | 112.5 | 111.8 | 105.3 | 92.0 |
| dev | 10 | 3 | 102.6 | 101.7 | 97.0 | 92.8 | 90.9 |
| dev | 100 | 1 | 96.7 | 91.5 | 88.9 | 89.7 | 89.4 |
| dev | 100 | 2 | 91.7 | 87.9 | 88.2 | 87.7 | 86.7 |
| dev | 100 | 3 | 95.8 | 94.0 | 89.6 | 91.5 | 90.7 |
| dev | 500 | 1 | 105.3 | 103.7 | 105.5 | 107.3 | 106.4 |
| dev | 500 | 2 | 101.4 | 101.0 | 104.6 | 104.0 | 101.1 |
| dev | 500 | 3 | 99.5 | 100.0 | 103.8 | 102.6 | 99.6 |
| fix | 1 | 1 | 99.4 | 105.2 | 122.8 | 121.4 | 101.0 |
| fix | 1 | 2 | 111.9 | 108.8 | 98.0 | 110.7 | 104.6 |
| fix | 1 | 3 | 102.9 | 93.9 | 95.0 | 115.6 | 121.7 |
| fix | 10 | 1 | 85.2 | 85.7 | 84.0 | 84.6 | 83.4 |
| fix | 10 | 2 | 85.2 | 86.1 | 87.7 | 88.8 | 85.8 |
| fix | 10 | 3 | 84.3 | 84.4 | 85.0 | 84.0 | 87.7 |
| fix | 100 | 1 | 86.1 | 83.1 | 85.9 | 83.7 | 86.9 |
| fix | 100 | 2 | 84.2 | 80.3 | 87.4 | 91.3 | 90.1 |
| fix | 100 | 3 | 93.1 | 82.1 | 83.1 | 84.2 | 84.2 |
| fix | 500 | 1 | 94.5 | 92.6 | 94.3 | 95.8 | 93.6 |
| fix | 500 | 2 | 97.1 | 93.7 | 97.1 | 96.3 | 98.1 |
| fix | 500 | 3 | 99.2 | 129.4 | 94.9 | 95.6 | 94.4 |
JDK 17, runner job j17-fix-then-dev (ran fix then dev)
| side | pipelineNum | fork | iter 1 | iter 2 | iter 3 | iter 4 | iter 5 |
|---|---|---|---|---|---|---|---|
| dev | 1 | 1 | 175.5 | 163.0 | 156.3 | 152.1 | 145.3 |
| dev | 1 | 2 | 187.5 | 178.0 | 169.5 | 144.6 | 188.4 |
| dev | 1 | 3 | 179.3 | 173.9 | 160.0 | 153.0 | 159.5 |
| dev | 10 | 1 | 91.1 | 88.6 | 88.5 | 86.7 | 86.5 |
| dev | 10 | 2 | 88.4 | 89.0 | 85.6 | 85.8 | 84.7 |
| dev | 10 | 3 | 90.5 | 89.1 | 85.4 | 85.4 | 85.2 |
| dev | 100 | 1 | 84.6 | 82.6 | 81.6 | 81.8 | 80.4 |
| dev | 100 | 2 | 84.0 | 82.8 | 81.1 | 81.9 | 80.0 |
| dev | 100 | 3 | 83.5 | 82.6 | 81.0 | 82.8 | 80.0 |
| dev | 500 | 1 | 103.2 | 97.9 | 97.0 | 98.4 | 98.0 |
| dev | 500 | 2 | 105.5 | 101.0 | 100.3 | 100.5 | 100.8 |
| dev | 500 | 3 | 98.8 | 97.5 | 100.6 | 98.1 | 97.3 |
| fix | 1 | 1 | 89.7 | 90.0 | 93.9 | 91.7 | 90.6 |
| fix | 1 | 2 | 99.3 | 109.1 | 100.3 | 91.6 | 92.8 |
| fix | 1 | 3 | 85.3 | 89.2 | 91.7 | 109.6 | 91.6 |
| fix | 10 | 1 | 73.6 | 73.1 | 78.4 | 77.2 | 77.2 |
| fix | 10 | 2 | 76.2 | 76.5 | 80.4 | 80.1 | 80.6 |
| fix | 10 | 3 | 74.2 | 72.6 | 75.7 | 76.4 | 76.2 |
| fix | 100 | 1 | 70.5 | 69.7 | 73.8 | 73.9 | 74.6 |
| fix | 100 | 2 | 75.2 | 74.4 | 74.4 | 74.7 | 76.0 |
| fix | 100 | 3 | 70.7 | 70.5 | 73.8 | 74.1 | 72.7 |
| fix | 500 | 1 | 91.6 | 90.4 | 90.1 | 92.4 | 91.6 |
| fix | 500 | 2 | 94.6 | 96.4 | 95.7 | 94.7 | 94.4 |
| fix | 500 | 3 | 89.6 | 92.6 | 96.1 | 92.5 | 93.9 |
Before I open a PR
Two choices in the proposed fix change how this benchmark runs, so I would like your view first:
-XX:CompileThresholdScaling=0.05changes the JIT's thresholds inside the benchmark. The alternative is to keep JVM defaults and warm up for about 150 s per trial instead. In the 300 s trajectories above that is where ten pipelines stops drifting, though I have not run it as a full sweep, and it leaves the one-pipeline body at a lower compilation tier than the other points. Which would you prefer?- Cost: the trigger-count warmup adds about 19 minutes per JDK to the
CheckpointSchedulingBenchmarksweep inbenchmarks.yml(about 200 s per fork at one pipeline, 60 s per fork elsewhere).
The one-pipeline point is kept and documented as an idle-member point. If you would rather drop it and start the sweep at ten pipelines, that removes the noisiest point and about half of the added time.
If this direction works for you, I will open it as a PR, or file an issue first if you prefer that. The fixed-interval scenario and longer measurement runs for tail percentiles stay a separate follow-up, which I will open as an issue once this is settled.
Purpose of this pull request
Adds
CheckpointSchedulingBenchmark, a JMH benchmark for the checkpointscheduling layer. Requested by @nzw921rx on
#12339 so that a change to the
scheduling model can be judged on measurements rather than on argument.
Today that layer is a blind spot.
CheckpointingTimeBenchmark#checkpointSingleInputgoes through
CheckpointBenchmarkTrigger, which reflects straight intocreatePendingCheckpointandstartTriggerPendingCheckpoint, so the scheduleris out of the measured path entirely, and checkpoint completion time is
dominated by the barrier round-trip anyway.
This benchmark measures how late a periodic checkpoint trigger runs after it is
due, as a distribution (
Mode.SampleTime: p50 / p99 / max, not only a mean),while sweeping the number of active pipelines on one member.
Fixture: a real member, real coordinators, fake tasks
temp directory), the IMaps, the checkpoint monitor service and the engine
context. IMap persistence is off: its write-through MapStore stalls checkpoint
id allocation for hundreds of milliseconds, which is not what is measured.
FakeTaskCheckpointManagerextendsCheckpointManagerand creates realCheckpointCoordinators through the existing constructor, onesingle-pipeline job each. Only the tasks are fake, at the existing
protected sendOperationToMemberNode: the fake reportsREADY_STARTandacknowledges each barrier. Every other operation gets an empty success.
measured, so the same benchmark runs unchanged on
devand on [Improve][Zeta] Share checkpoint trigger scheduling across active pipelines #12165.No engine code change, no Mockito, no new dependency.
Measurement boundary
@Setup(Level.Invocation), not timed: picks the coordinator due soonest,parks until shortly before it is due, then spins to the due time.
@Benchmark, timed: spins until that coordinator'spendingCountergoes from0 to 1, i.e. until the trigger has created its pending checkpoint.
The score is the scheduling delay plus the trigger's own decision logic up to
creating the checkpoint. The due time is "previous observation + interval",
which is a few microseconds early because the trigger body increments
pendingCounterjust before re-arming. The offset is identical on everyscheduler compared; the reason is a comment at the spin loop.
Keeping the samples honest
100 ms apart; the rest are load. Measuring all of them would drop whichever
trigger came due while another was being measured, and those are the
triggers that bunch up under contention, which biases the result towards
short delays where the scheduler is under the most pressure. Which
coordinators are probes is fixed by index, so the samples carry no such
selection.
count is printed every iteration, pass or fail, split by cause: checkpoint
already pending, came due during another sample, parking overran, ran before
its estimated due time. An iteration with more than 10% skipped fails the
run instead of reporting a number. The limit applies once an iteration has at
least 50 due triggers, so the CI job's one-second smoke iteration, which sees
only a handful, is not failed by a single skip.
pipelineNum * 20 ms, floored at 200 ms, so every point of thesweep sees the same trigger rate and storage load. 500 pipelines gives 10 s.
Engine internals it depends on
Two fields are read by reflection, read-only:
CheckpointCoordinator.pendingCounterandCheckpointBarrierTriggerOperation.barrier(no getter). The fake also encodestwo task messages. Each has a comment at its read site, and a missing field
fails the benchmark at setup with the class and field named.
Tooling change:
save_jmh_result.pyFor sample modes JMH writes raw data as
rawDataHistogram, notrawData.jmh_metrics()only readrawData, so everySampleTimebenchmark wasreported with CV = 0%. No existing benchmark used
SampleTime, which is whythis had not shown up. Each iteration's histogram is now reduced to its mean,
so CV keeps meaning variation between iterations as it does for the other
modes. Benchmarks that publish
rawDataare unchanged.Does this PR introduce any user-facing change?
No. Benchmark and benchmark-tooling code only.
How was this patch tested?
CheckpointSchedulingFixtureTestruns the fixture on a real member: duetriggers of real coordinators are measured, teardown leaves none of the
fixture's scheduler threads behind (a pre-existing
checkpoint-*thread fromelsewhere in the JVM is ignored), parameters are validated, probe spacing is
computed as documented, the skip limit applies only once enough triggers were
due, and a missing engine field is reported by name.
CheckpointSchedulingBenchmarkTestchecks the JMH mode, unit and thread count. The newtest_save_jmh_resultcase failed before the fix.Full sweep, local, one fork, 1 warmup + 3 measurement iterations of 10 s,
devvs #12165 headc773cc091, same benchmark code on both:devdevThe scheduler thread counts in this table are lower bounds. They were taken
before trial setup waited for every coordinator to trigger once, when it waited
only for the measured ones, so a few load coordinators could still be arming
their first trigger when the threads were counted. The benchmark now prints the
full count.
Delay numbers are deliberately not in this table: on macOS every timed wait
fires about 5 ms late, for a plain
ScheduledThreadPoolExecutortoo, so thelocal distribution sits on the OS timer floor. The delay comparison will come
from the Linux runners through the
benchmarks.ymlentry.Check list
New License Guide
incompatible-changes.mdto describe the incompatibility caused by this PR.No new dependency, no user-facing change, no incompatibility, no connector
code.
docs/en/engines/zeta/benchmark.mdcovers running the benchmark suiteand needs no per-benchmark section.