Repository navigation
Conversation
…ath hash Replace the project-path hash used by `-Pslot=X/Y` with a deterministic greedy longest-processing-time-first bin-packing seeded from per-project test durations harvested from CI Visibility (test-shard-weights.json). Projects missing from the weights file fall back to a default weight. The assignment is computed once per slot count from all project paths, so every shard agrees on it, and cached on the root project. Solves APMCI-45. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: cgcote <5115638+cgcote@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Contributor
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
MuzzleAction cached a single boot/tool classloader pair per build and only rebuilt it when the bootstrap + tooling class-path file count grew. A project whose muzzleBootstrap differs from, but is not larger than, the previous project's therefore reused the wrong bootstrap classloader: - graal-native-image-20.0 adds agent-installer to muzzleBootstrap; - opentelemetry-1.47 adds otel-bootstrap, with fewer files; - when graal ran first in the same worker, opentelemetry-1.47 reused its classloader and failed helper injection with NoClassDefFoundError: datadog/trace/bootstrap/otel/metrics/OtelInstrument. The reverse also held: projects with a smaller bootstrap class-path could see classes they never declared. Under project-path hashing, graal and the OpenTelemetry projects never shared a muzzle shard; duration-based shard assignment put graal-native-image-20.0 and opentelemetry-1.47 together in slot 5, exposing it. Cache the classloaders per distinct class-path for the current build so every muzzle check sees exactly its own declared bootstrap class-path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: cgcote <5115638+cgcote@users.noreply.github.com>
Co-authored-by: cgcote <5115638+cgcote@users.noreply.github.com>
…-duration Co-authored-by: cgcote <5115638+cgcote@users.noreply.github.com>
The first pipeline using duration-based shards did not speed up the slowest test_inst shard: the 24-entry weights file missed lettuce-5.0, the second heaviest project, which then shared slot 3 with spring-webflux-5.0. - Replace the weights with 36 projects measured one by one from CI Visibility (same query, JVM 11, 7 days, 491 pipelines), and correct the unit to a per-pipeline mean. - Raise DEFAULT_WEIGHT_SECONDS to 20.6, the mean of the projects left out. - Test that the two heaviest projects never share a slot and that every weighted path is an included project. - Document that a shard can never finish faster than its heaviest project (jdbc, then lettuce-5.0), so the modeled balance is optimistic when one project approaches the target shard time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: cgcote <5115638+cgcote@users.noreply.github.com>
…ns-1.20 otel-shim, an implementation dependency of this module, is compiled against otel-bootstrap classes that the agent loads on the bootstrap class-path. Declare them for muzzle like the sibling opentelemetry modules do, so muzzle checks this module against the same bootstrap classes. This is a consistency improvement only: muzzle passes for this module with and without the declaration, and it was not the cause of the muzzle: [5/8] failure, which came from the muzzle classloader cache. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: cgcote <5115638+cgcote@users.noreply.github.com>
This was referenced Oct 12, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Does This Do
Each Gradle project used to get its CI shard (
-Pslot=X/Y) fromabs(project.path.hashCode() % totalSlots) + 1. This PR replaces that with a deterministic, duration-aware greedy LPT (longest-processing-time-first) bin-packing:buildSrc/src/main/resources/datadog/gradle/plugin/ci/test-shard-weights.jsonholds per-projecttest_instweights for the 36 heaviest projects. Each weight is a mean per pipeline: CI Visibilitysum(@duration)at module level, JVM 11, 7 days, divided by 491 pipelines. Any other project getsDEFAULT_WEIGHT_SECONDS = 20.6, the mean of the projects left out.TestShardBalanceris pure Kotlin with no Gradle types. It sorts projects by weight, heaviest first, breaking ties by path. Each project goes into the least loaded slot, breaking ties by lowest index. Hash codes are never used, so the result is the same on every JVM and machine.isInSelectedSlotcomputes the assignment once per slot count from the sortedrootProject.allprojectspaths, so every shard agrees on it. It's cached onrootProject.extrabehind a lock. All early returns and the info log line are unchanged.docs/ci-test-shard-weights.mdcovers how to regenerate the weights (measuring each project individually), when to refresh them, and the known limitation below.MuzzleActionkept one bootstrap/tooling classloader pair per build. It was only rebuilt when the classpath file count grew.opentelemetry-annotations-1.20now declaresmuzzleBootstrap otel-bootstraplike its sibling OpenTelemetry modules. This is a consistency improvement only: muzzle passes with and without it, and it did not cause the muzzle failure.The split stays 8-way, and
.gitlab-ci.ymlis unchanged.Motivation
Work item: APMCI-45.
test_instruns as 8 shards, and thetestsstage ends when the slowest one does. The hash split balanced project counts, but not durations.Measured, JVM 11
test_inst. Before is the CI Visibility p50 with the hash split; run 1 is the first pipeline with this PR, using the original 24-entry weights:Run 1 did not improve the slowest shard: ~41 min, against a ~40 min baseline. The max/min spread fell from 2.2x to 1.7x, but:
lettuce-5.0was missing from the original weights. It is the second-heaviest project, at about 978 s per pipeline. As an "unknown" project it got the default weight and landed in slot 3 next tospring-webflux-5.0. Shard 3 ran 4105 s of tests, against 1525–2312 s for the others.jdbcandlettuce-5.0in separate slots.Known limitation: the heaviest project sets a floor. A project's tests always run in one shard, and tests run in parallel within a shard. The balancer adds up seconds and cannot see this.
jdbc. Its tests (20.5 min of summed test time) spanned 27.6 min of wall clock and started about 10 min into the job.jdbcwas in slot 5, the old slowest shard at 40m11s.jdbcalone, and this PR can only gain a minute or two on merge-ready time. Thelettuce-5.0shard (16.3 min of tests per pipeline) is the next floor.TestShardBalancerTesttarget of max/min ≤ 1.25 is a check on the model, not a wall-clock guarantee.jdbcshard.Why the muzzle fix is needed. On the first pipeline,
muzzle: [5/8]failed: everyopentelemetry-1.47check hitNoClassDefFoundError: datadog/trace/bootstrap/otel/metrics/OtelInstrument, although 1.47 declaresotel-bootstrap.graal-native-image-20.0addsagent-installer, a larger but different bootstrap classpath, and ran first in the same worker.opentelemetry-1.47then reused graal's loader, which has nootel-bootstrap.Testing
TestShardBalancerTest(28 cases), using the real project paths fromsettings.gradle.kts. It covers:settings.gradle.ktswith a build file.MuzzleActionTestfails with the old count-based cache and passes with the fix;opentelemetry-1.47, on a single worker) went from 24/24 failing to 24/24 passing;opentelemetry-annotations-1.20passes 24/24 with the new declaration../gradlew :buildSrc:test -PrunBuildSrcTests: all balancer and muzzle tests pass, includingMuzzlePluginFunctionalTest20/20. 16 tests inBuildTimeInstrumentationPluginTest,CallSiteInstrumentationPluginTest,DumpHangedTestIntegrationTestandTestJvmCacheInputsTestfail only in the sandbox, because their nested TestKit builds can't resolve dependencies. They fail the same way without this PR, andcheck_build_srcpasses in CI.Additional Notes
akka-http-10.6is only included withakkaRepositoryToken, which CI sets for every job.Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issueJira ticket: [APMCI-45]
🤖 Generated with Claude Code (link removed)
PR by Bits - View session in Datadog
Comment @DataDog to request changes