Skip to content

Skip the client response-time Observation when no tracing is configured - #4328

Open
samueldlightfoot wants to merge 2 commits into
reactor:mainfrom
samueldlightfoot:client-metrics-recording-perf
Open

samueldlightfoot wants to merge 2 commits into
reactor:mainfrom
samueldlightfoot:client-metrics-recording-perf

Conversation

@samueldlightfoot

@samueldlightfoot samueldlightfoot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Change

With .metrics(true) and no tracing configured, the client runs a full Micrometer Observation per request to
record one timer. Nothing consumes it. It is 43.4% of the allocation that enabling metrics adds to the client
event loop.

When the ObservationRegistry is the untouched built-in default, startWrite now skips the Observation and
recordRead records the response.time Timer directly. Same meter, name, tags and value.

Deciding "untouched" needs Reactor Netty to own the registry: ObservationConfig's accessors are
package-private, and the documented tracing setup mutates the default registry in place rather than replacing
it, so registry identity alone can't answer it. DEFAULT_OBSERVATION_REGISTRY tracks whether any handler,
predicate, filter or convention was registered after Reactor Netty installed its own. Registering any of them,
or assigning OBSERVATION_REGISTRY, re-engages the full lifecycle.

The response-time meter name is also precomputed on the recorder rather than rebuilt twice per request.

Behaviour

Two visible deltas, both intended:

  • With the untouched default registry there is no longer an Observation on the channel context, so code
    reading OBSERVATION_KEY off a client channel finds nothing. Registering any handler, predicate, filter
    or convention — or assigning OBSERVATION_REGISTRY — restores it.
  • The REMOTE_ADDRESS tag on response.time now uses formatSocketAddress, like every other client meter,
    instead of netPeerName + ":" + netPeerPort. For an InetSocketAddress the two are identical. For other
    remotes — a domain socket, say — the tag loses the trailing : the old form left when the port was empty.
    That is a meter-identity change for those users. The NET_PEER_NAME and NET_PEER_PORT high-cardinality
    tags are unchanged.

Benchmarks

Rig: H2C loopback, 32 threads, JDK 17, 180 s after a 30 s warmup, SimpleMeterRegistry on the global
composite, metrics on in both arms, back-to-back in one session. Allocation sampled at a fixed --alloc 512k
interval.

Benchmark Metric Before After Δ
Rig A/B, full client loop Client-loop allocation 102,193 samples 92,270 samples −9.7%
Rig A/B, full client loop Throughput 94,923 req/s 97,671 / 98,719 req/s +3–4%

@samueldlightfoot

Copy link
Copy Markdown
Contributor Author

@violetagg keen for your thoughts on this. It could be a big win out of the box for those wanting metrics with no use for observations.

When HTTP client metrics are enabled but no tracing consumer is registered,
the built-in Micrometer recorder still ran a full per-request Observation
solely to record the response-time timer, allocating a per-request
ConcurrentHashMap, Timer.Sample and ResponseTimeHandlerContext and writing the
channel context — none of which anything consumed.

Reactor Netty now owns the default ObservationRegistry so it can tell whether
any observation handler, predicate, filter or convention has been registered:
the ObservationConfig accessors are package-private and the documented tracing
setup mutates the default registry in place, so registry identity alone is not
sufficient. When the registry is the untouched default, the recorder records
the response-time timer directly — same meter, name, tags and value — and skips
the Observation lifecycle. Any customization re-engages the full lifecycle, so
tracing is unaffected.

Signed-off-by: samlightfoot <samueldlightfoot@gmail.com>
@samueldlightfoot
samueldlightfoot force-pushed the client-metrics-recording-perf branch from cb0b4e3 to 1b249bf Compare August 6, 2026 15:54
@violetagg

Copy link
Copy Markdown
Member

I'm ok with that, but I don't feel like introducing it to the 1.3.x version. It should go to the 1.4.x version (currently the main branch). Wdyt?
I won't be able to review it for 1.4.0-M1, so it will most likely be for 1.4.0-M2.

@violetagg violetagg added the type/enhancement A general enhancement label Aug 6, 2026
@samueldlightfoot

Copy link
Copy Markdown
Contributor Author

I'm ok with that, but I don't feel like introducing it to the 1.3.x version. It should go to the 1.4.x version (currently the main branch). Wdyt? I won't be able to review it for 1.4.0-M1, so it will most likely be for 1.4.0-M2.

SGTM - no rush!

@violetagg violetagg added this to the 1.4.x Backlog milestone Aug 6, 2026
@samueldlightfoot
samueldlightfoot marked this pull request as ready for review August 9, 2026 09:47
Match TrackedObservationConfig's overrides against ObservationConfig's
mutators by full signature rather than name, so an added overload of an
existing mutator fails the guard instead of silently bypassing detection,
and assert the discovered set is non-empty so a covariant return type
upstream cannot make the check vacuous.

Also use the precomputed responseTimeName in both recordResponseTime
overloads, which the original change missed.

Signed-off-by: samlightfoot <samueldlightfoot@gmail.com>
@samueldlightfoot
samueldlightfoot force-pushed the client-metrics-recording-perf branch from cb888b1 to 3f48655 Compare August 9, 2026 10:13
@samueldlightfoot samueldlightfoot changed the title Skip the client response-time Observation when no tracing is configured [DRAFT] Skip the client response-time Observation when no tracing is configured Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants