Skip to content

Introduce Maybe<T> an allocation free return value for fallible operations - #12328

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 15 commits into
masterfrom
dougqh/apmlp-1799-try-t-pr
Aug 28, 2026
Merged

Introduce Maybe<T> an allocation free return value for fallible operations#12328
gh-worker-dd-mergequeue-cf854d[bot] merged 15 commits into
masterfrom
dougqh/apmlp-1799-try-t-pr

Conversation

@dougqh

@dougqh dougqh commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Adds Maybe<T>: a present/absent wrapper for fallible operations (e.g. a capacity-refusing table lookup)

Motivation

Allowing update/mutation calls without worrying about accidentally raising NullPointerException.
Maybe<T> is carefully designed to be scalar replaced via escape analysis in all supported JVMs.

Additional Notes

Confirmed allocation-free (JDK 8/11/17/25), provided the wrapping method constructs the Maybe at exactly one call site — see the class javadoc and MaybeUsagePatternsBenchmark for the single- vs multi-construction-site contrast that makes that discipline concrete rather than a claim. Note: the multi-construction-site "bad" arm measures 16 B/op on JDK 8/11/17/21, but ~0 B/op on JDK 25, where ReduceAllocationMerges happens to collapse this specific two-same-type-branch shape — documented in the class javadoc as a JDK-25-only behavior not to be relied on.

Created in response to Hashtable's capacity-refusing operations, which forced a choice between an NPE footgun (returning a plain nullable) and a combinatorial explosion of method overloads. Maybe<T> bridges the gap by becoming the logical home for mutation/update methods.

MaybeUsagePatternsBenchmark is also meant as the backing example for a perf-review check like "EA-dependent elision on a hot path where a structural alternative exists at parity → prefer the deterministic form" (APMLP-1799's J12 note): its boxed-context arm shows 0 B/op only while its call site stays inlined, versus the primitive-context overload's 0 B/op unconditionally.

update's primitive-context form is long-only. An int/double/boolean sibling was tried and reverted after review (Codex + Datadog Autotest both caught it): a second primitive overload makes inline-lambda calls to update genuinely ambiguous, confirmed by direct compilation — JLS 15.12.2.5's most-specific-method rule requires every parameter position to agree, and ObjIntConsumer/ObjLongConsumer/ObjDoubleConsumer are unrelated interfaces, so the functional-interface parameter blocks resolution even though the numeric parameter alone would resolve via widening. See the javadoc on update(long, ObjLongConsumer) for the full reasoning.

Test plan

  • ./gradlew :internal-api:compileJava :internal-api:compileJmhJava — clean
  • ./gradlew :internal-api:spotlessCheck — clean
  • ./gradlew :internal-api:test --tests datadog.trace.util.MaybeTest — clean, covers both of overloads, isPresent/getOrNull, all three update overloads (present + absent), and ifPresentOrElse
  • ./gradlew :internal-api:jacocoTestCoverageVerification — clean (per-class branch/instruction coverage gate satisfied)
  • JMH run of MaybeUsagePatternsBenchmark.badMultiConstructionSite across JDK 8/11/17/21/25 — 16 B/op on 8-21, ~0 B/op on 25 (see Additional Notes)
  • Sanity JMH run of MaybeUsagePatternsBenchmark's other arms (JDK 17) — good arms read ≈0 B/op, boxed-context bad arm reads the expected non-zero allocation (24 B/op uninlined)
  • Full JDK 8/11/17/25 sweep of the remaining MaybeUsagePatternsBenchmark arms (already done for the core Maybe/EscapeShapeBenchmark shapes; not yet re-run for every arm in this specific demonstration file)

🤖 Generated with Claude Code

dougqh and others added 10 commits August 27, 2026 16:17
Fourteen minimal arms, each isolating one thing believed to decide whether
C2 can delete a short-lived object, read through gc.alloc.rate.norm. The
suite was written against APMLP-1642's granted/refused reservation and
carries over unchanged, because a Try<T> is the same two-outcome wrapper
and what is measured is the shape, not the caller.

It answers part of APMLP-1799's section A ahead of time: the Optional-style
merge with a singleton costs 8 B/op on 25 as well as 17, since
ReduceAllocationMerges never covers a static input; a merge of two
allocations, which is what Success/Failure compiles to, stays 16 on 25; and
three receiver types at one call site cost 24. One allocation site carrying
a flag is 0 on both, with or without try/finally. Crossing a call boundary
is free when the callee inlines and 24 B/op when it does not.

The JDK 8, 11 and 21 columns are unfilled, there is no EA-off control arm
and no type-profile pollution, so this is not yet the card's deliverable 2 —
it is a running start on it. Cold-path escape and callee size, the two arms
that could still kill the wrapper, are not probed at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ran the suite on this machine's Zulu 8.72.0.17 with -Pjmh.fork=1,
-prof gc. Every arm matches its 17/25 reading, including phiWithNull
staying at 8 B/op rather than dropping to 0 -- the ReduceAllocationMerges
relaxation only applies from JDK 21, so JDK 8 sees the older, unconditional
merge-with-null cost.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
APMLP-1799 spike: a generic fallible-operation return shape (single
construction site, plain nullable field, no Optional-style singleton
merge) confirmed allocation-free on JDK 8/11/17/25 by
EscapeShapeBenchmark. Not wired into any real caller yet -- Hashtable
usage lands in a follow-up PR once it can build on this.
Confirmed 2026-08-27 against a real capacity-refusing lookup method
(JDK 8/11/17/25, common and rare refusal ratios): the capturing lambda
scalar-replaces as reliably as a plain delegating call. Full benchmark
lands with the Hashtable-integration follow-up.
Both flavors are named in the ticket's mutator-flavor set
(Consumer / BiConsumer<C> / ObjLongConsumer / ObjIntConsumer) that
motivated the N*M overload explosion on Hashtable. Consolidating them
onto Try means that shape is paid for once here instead of once per
table type. double/boolean context forms are deliberately not added:
neither is a named flavor, and boolean has no JDK ObjBooleanConsumer
to reuse -- both would be speculative additions without a real caller.
Rounds out the primitive-context set to int/long/double/boolean,
anticipating future callers rather than waiting on one, matching how
Stream/Optional carry int/long/double specializations. boolean has no
JDK ObjBooleanConsumer to reuse (neither does Stream/Optional -- the
JDK never shipped a boolean specialization there either), so it's a
small hand-rolled functional interface instead.
Completes the mutator-flavor set named in the ticket (Consumer /
BiConsumer<C> / ObjLongConsumer / ObjIntConsumer) plus the
anticipated double/boolean siblings. Argument order matches
Hashtable#forEach's existing (context, entry) convention rather than
the primitive forms' (entry, context), since that's the precedent
this is meant to line up with.
Standalone JMH demonstration (not a research instrument like
EscapeShapeBenchmark) meant to back a perf-review check like
APMLP-1799's J12 note. Two pairs:

- goodSingleConstructionSite vs badMultiConstructionSite: the
  single-construction-point discipline from Try's class javadoc,
  made visible as 0 vs 16 B/op rather than left as a claim.
- goodPrimitiveContextUpdate vs badBoxedContext{Inlined,Uninlined}:
  update(long, ObjLongConsumer) reads 0 B/op deterministically;
  update(Long, BiConsumer) also reads 0 B/op but only because this
  call site stays inlined and C2 scalar-replaces the box -- forcing
  the same call out of line (CompileCommand=dontinline, the same
  technique EscapeShapeBenchmark uses for UninlinedStrategy) shows
  the real 24 B/op the primitive overload avoids unconditionally.
Try collides with Scala/Vavr's Try<T>, which carries a captured
exception (Success/Failure) -- a model this class never had and, by
design, never will: it's a plain present/absent wrapper with no error
payload, i.e. Haskell's Maybe (Just/Nothing) rather than Either or
Result. The name should say so rather than borrow one that implies
exception handling.
The previous commit only captured the file renames -- the actual
Try -> Maybe identifier/javadoc replacements were left uncommitted by
a failed `git add -A` (stale pathspec aborted the whole add silently).
@dougqh dougqh added type: feature Enhancements and improvements comp: core Tracer core tag: no release notes Changes to exclude from release notes tag: ai generated Largely based on code generated by an AI or LLM labels Aug 28, 2026
@dougqh

dougqh commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@datadog-official

datadog-official Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 58.81% (+0.00%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 9296289 | Docs | View more details | Give us feedback!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec1419151e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal-api/src/jmh/java/datadog/trace/util/MaybeUsagePatternsBenchmark.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java Outdated
@dd-octo-sts

dd-octo-sts Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.08 s 14.02 s [-0.4%; +1.2%] (no difference)
startup:insecure-bank:tracing:Agent 12.98 s 13.06 s [-1.2%; -0.1%] (maybe better)
startup:petclinic:appsec:Agent 16.91 s 16.65 s [+0.8%; +2.3%] (maybe worse)
startup:petclinic:iast:Agent 16.92 s 16.91 s [-0.9%; +0.9%] (no difference)
startup:petclinic:profiling:Agent 16.67 s 16.12 s [-1.1%; +7.9%] (no difference)
startup:petclinic:sca:Agent 16.74 s 16.60 s [-0.1%; +1.8%] (no difference)
startup:petclinic:tracing:Agent 16.07 s 16.00 s [-0.3%; +1.2%] (no difference)

Commit: 92962893 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@dougqh dougqh changed the title Introduce Maybe<T> as an allocation-free fallible-result primitive Introduce Maybe<T> an allocation free return value for fallible operations Aug 28, 2026
The multi-construction-site claim ("fails scalar replacement on every
JDK 8-25") was wrong: measured badMultiConstructionSite directly on
JDK 8/11/17/21/25 -- 16 B/op on 8-21, but ~0 B/op on 25, where
ReduceAllocationMerges collapses two branches allocating the same
final type. Documented the JDK-25 exception and why it's not something
to rely on. Also corrected the capturing-lambda javadoc (a capturing
lambda is freshly instantiated per call, not "built once per call
site") and the stale TryUsagePatterns JMH filter in the class doc.
@dougqh
dougqh marked this pull request as ready for review August 28, 2026 03:31
@dougqh
dougqh requested a review from a team as a code owner August 28, 2026 03:32
@dougqh
dougqh requested review from vandonr and removed request for a team August 28, 2026 03:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f40ced4c9a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal-api/src/jmh/java/datadog/trace/util/MaybeUsagePatternsBenchmark.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java
Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java Outdated

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: FAIL

The numeric update methods make a direct lambda call ambiguous. Callers must cast or declare the consumer, so the intended primitive-context API does not compile in its natural form.

Open Bits AI session

🤖 Datadog Autotest · Commit f40ced4 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java Outdated

@AlexeyKuznetsov-DD AlexeyKuznetsov-DD left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, left minor comments and one idea.

Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java

@bric3 bric3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My untrained eye tripped over a few things, please bear with me.

Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java
Comment thread internal-api/src/main/java/datadog/trace/util/Maybe.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/escape/EscapeShapeBenchmark.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/escape/EscapeShapeBenchmark.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/MaybeUsagePatternsBenchmark.java Outdated
dougqh and others added 2 commits August 28, 2026 09:43
- Drop the int/double/boolean update overloads: confirmed by direct
  compilation that a second primitive overload makes inline-lambda
  calls ambiguous (JLS 15.12.2.5's most-specific-method rule requires
  every parameter position to agree, and ObjIntConsumer/ObjLongConsumer/
  ObjDoubleConsumer are unrelated interfaces). Keep only update(long,
  ObjLongConsumer) -- flagged independently by Codex and the Datadog
  Autotest bot. Removes the now-dead ObjBooleanConsumer.
- Qualify MaybeUsagePatternsBenchmark's zero-allocation claim: the
  boxed-context pair's 0 B/op is contingent on the update call itself
  staying inlined, not immune to every inlining failure.
- Add MaybeTest covering every branch of Maybe (both of() overloads,
  isPresent/getOrNull, all three update overloads, ifPresentOrElse)
  so internal-api's per-class JaCoCo coverage gate passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per review nit: that context belongs in the PR description, not a
comment that will rot as soon as a caller adopts it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dougqh and others added 2 commits August 28, 2026 10:36
- Rename EscapeShapeBenchmark's domain-flavored types (Cell/Granted/
  Alternate/REFUSED/Flagged) to neutral names (Outcome/SingleAllocation/
  AlternateAllocation/STATIC_SINGLETON/FlaggedAllocation) so the
  benchmark reads as a compiler-behavior experiment, not a tracer
  reservation model.
- Drop the APMLP-1642/APMLP-1799 JIRA references from the class
  javadoc in favor of a generic two-outcome-wrapper description.
- Add a short glossary paragraph (escape analysis, scalar replacement,
  ReduceAllocationMerges) and note the results are HotSpot/C2-specific,
  not validated against OpenJ9/GraalVM.
- Rename MaybeUsagePatternsBenchmark's ADD_BOXED to ADD_BOXED_INLINED
  for symmetry with ADD_BOXED_UNINLINED.
- Trim the dated "Confirmed 2026-08-27" research-note phrasing out of
  Maybe's javadoc, keeping the substance without the research-log tone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lead the glossary paragraph with plain-language framing before naming
EA/scalar-replacement/ReduceAllocationMerges, drop "phi" in favor of
"merge" throughout (prose, table, and benchmark method names), and
explain the merge case with a concrete if/else example instead of SSA
terminology.
@dougqh
dougqh added this pull request to the merge queue Aug 28, 2026
@dd-octo-sts

dd-octo-sts Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Aug 28, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-08-28 17:49:34 UTC ℹ️ Start processing command /merge


2026-08-28 17:49:39 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2h (p90).


2026-08-28 18:47:42 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 28, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 6749995 into master Aug 28, 2026
601 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the dougqh/apmlp-1799-try-t-pr branch August 28, 2026 18:47
@github-actions github-actions Bot added this to the 1.66.0 milestone Aug 28, 2026
dougqh added a commit that referenced this pull request Aug 29, 2026
Maybe/MaybeTest/EscapeShapeBenchmark/MaybeUsagePatternsBenchmark, copied
verbatim from the merge-queued PR #12328, so this branch (stacked on
#12101) can add Maybe-returning Hashtable/FlatHashtable methods without
waiting on the queue. Drop this commit's contents in favor of master's
copy once this branch rebases past #12328 landing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants