perf(cache)!: take the remaining allocations off the Redis command path - #215
cosmin-staicu wants to merge 1 commit into
Conversation
|
🔎 Maintainer heads-up: automated triage flagged this PR as potentially material, so it may need a signed CLA in addition to the DCO sign-off. Strong signals
Other signals
This is advisory only — the bot does not decide. Please judge against the CLA criteria (material, product-critical, patent-sensitive, corporate contributor, broad commercial use). Note that thresholds can be gamed by splitting PRs, so use your judgement.
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Three moderate findings remain unresolved, covering JSON validation, serializer overload resolution, and pooled-buffer cleanup.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This breaking PR reduces Redis command-path allocations through byte-based keys, stateful callbacks, pooled serialization buffers, and struct-based telemetry.
Changes:
- Adds byte-prefix Redis keys and state-aware resilience callbacks.
- Introduces pooled
SerializedPayloadwrite buffers. - Replaces telemetry operation objects with
TelemetryScope. - Updates tests, documentation, and public API records.
| File | Summary |
|---|---|
tests/UiPath.Caching.Tests/Telemetry/TelemetryScopeTests.cs |
Tests struct-based telemetry scopes. |
tests/UiPath.Caching.Tests/Telemetry/TelemetryOperationTests.cs |
Updates legacy telemetry operation coverage. |
tests/UiPath.Caching.Tests/SerializedPayloadTests.cs |
Tests pooled payload behavior. |
tests/UiPath.Caching.Tests/ResiliencePipelineWrapperTests.cs |
Tests stateful pipeline execution. |
tests/UiPath.Caching.Tests/ResiliencePipelineStateOverloadTests.cs |
Tests default interface compatibility. |
tests/UiPath.Caching.Tests/Redis/RedisHashCacheTests.cs |
Updates Redis hash behavior coverage. |
tests/UiPath.Caching.Tests/Redis/RedisCacheTests.cs |
Updates Redis cache behavior and telemetry coverage. |
tests/UiPath.Caching.Tests/Redis/ConnectorKeyPrefixTests.cs |
Tests preserved connector prefix bytes. |
src/UiPath.Caching/Redis/ShardPrefixRedisKeyStrategy.cs |
Uses byte-based key prefixes. |
src/UiPath.Caching/Redis/RedisHashCache.cs |
Uses state callbacks and pooled hash writes; rented buffers need cleanup on pre-write failures. |
src/UiPath.Caching/Redis/RedisCacheBase.cs |
Updates prefix and telemetry handling. |
src/UiPath.Caching/Redis/RedisCache.cs |
Uses pooled writes and state callbacks; multi-key telemetry allocation should check IsEnabled. |
src/UiPath.Caching/Redis/PrefixRedisKeyStrategy.cs |
Stores encoded prefixes. |
src/UiPath.Caching/Redis/ConnectorKeyPrefix.cs |
Preserves resolved prefix bytes. |
src/UiPath.Caching/PublicAPI.Unshipped.txt |
Records API changes. |
src/UiPath.Caching/Broadcast/Redis/RedisStreamsTopic.cs |
Uses stateful callbacks. |
src/UiPath.Caching/Broadcast/Redis/RedisPubSubTopic.cs |
Uses stateful callbacks. |
src/UiPath.Caching.Queue/RedisSetCache.cs |
Uses stateful Redis callbacks. |
src/UiPath.Caching.Polly/ResiliencePipelineWrapper.cs |
Adapts state callbacks to Polly. |
src/UiPath.Caching.Abstractions/Telemetry/TelemetryScope.cs |
Adds struct-based telemetry timing. |
src/UiPath.Caching.Abstractions/Telemetry/TelemetryOperation.cs |
Removes the legacy telemetry operation. |
src/UiPath.Caching.Abstractions/Telemetry/NullTelemetryProvider.cs |
Updates no-op telemetry support. |
src/UiPath.Caching.Abstractions/Telemetry/NullTelemetryOperation.cs |
Removes legacy null operation support. |
src/UiPath.Caching.Abstractions/Telemetry/ITelemetryOperation.cs |
Removes the legacy telemetry interface. |
src/UiPath.Caching.Abstractions/Telemetry/ICachingTelemetryProvider.cs |
Updates the telemetry provider contract. |
src/UiPath.Caching.Abstractions/SystemJsonByteSerializerProxy.cs |
Adds pooled serialization; method lookup must handle derived overloads unambiguously. |
src/UiPath.Caching.Abstractions/SerializedPayload.cs |
Adds disposable pooled payload ownership. |
src/UiPath.Caching.Abstractions/PublicAPI.Unshipped.txt |
Records abstraction API changes. |
src/UiPath.Caching.Abstractions/PooledJsonWriter.cs |
Adds pooled JSON writing; validation must remain enabled for byte-compatible output. |
src/UiPath.Caching.Abstractions/Policies/IResiliencePipeline.cs |
Adds state-aware execution overloads. |
src/UiPath.Caching.Abstractions/Policies/EmptyResiliencePipeline.cs |
Implements state-aware execution. |
src/UiPath.Caching.Abstractions/IMemorySerializerProxy.cs |
Adds the payload serialization contract. |
docs/reference/interfaces.md |
Updates interface documentation. |
docs/how-to/telemetry-and-strategies.md |
Documents telemetry migration. |
docs/concepts.md |
Updates telemetry terminology. |
CHANGELOG.md |
Documents breaking and performance changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
6dc84c9 to
0b670d8
Compare
0b670d8 to
0eadcc9
Compare
Benchmark across the stackThese are the three PR heads measured in one session with the same harness: #213 ( Bytes allocated per call
At #213, telemetry off allocates as much as telemetry on, because Time per call
|
0eadcc9 to
c6a32b2
Compare
c6a32b2 to
8491ac8
Compare
c8e5f8c to
17f2e66
Compare
8491ac8 to
7af5298
Compare
7af5298 to
af63e51
Compare
73115bd to
c292ef2
Compare
ce4cbe4 to
5835c59
Compare
c292ef2 to
b8965d3
Compare
5835c59 to
b75b360
Compare
b75b360 to
54a8bf5
Compare
54a8bf5 to
a80cae6
Compare
a80cae6 to
51fdd9b
Compare
95ae7db to
db70dcb
Compare
db70dcb to
e332441
Compare
- PrefixRedisKeyStrategy encodes its prefix once and returns a RedisKey that carries it as a byte prefix next to the caller's name, so no key string is built per command. The cross-slot check uses the connector prefix bytes ConnectorKeyPrefix already reads. - IResiliencePipeline gains ExecuteAsync<TResult, TState>, which the caches and topics call with static lambdas. The Polly wrapper boxes the state once, since Polly copies its state into each strategy's state machine. - With the built-in JSON serializer, writes serialize into a buffer rented from ArrayPool<byte>.Shared and return it once the write has completed. The pooled payload stays internal, so no caller can return a buffer twice. The bytes match Serialize, and a subclass of the JSON proxy, like any other serializer, lends its own memory. - The caches time each operation with TelemetryScope, a struct read from the cache's TimeProvider, instead of allocating a TelemetryOperation. - TelemetryScope and the pooled writer live in UiPath.Caching, since the abstractions package holds only interfaces, null implementations and DTOs. BREAKING CHANGE: PrefixRedisKeyStrategy.Prefix and Separator lose their protected setters. ITelemetryOperation, TelemetryOperation, NullTelemetryOperation, ICachingTelemetryProvider.StartOperation and NullTelemetryProvider's StartOperation overloads are removed, and RedisCacheBase.TrackRead takes a TelemetryScope. A telemetry provider that implements only the Track methods is unaffected. RedisCacheBase.SupportsExpireTime resolves the server version on first use instead of in the constructor, so constructing a cache that never reads an expiration, such as RedisSetCache, no longer connects. A surrogate separator is refused with ArgumentException: encoded apart from the key name it could no longer pair with a name that starts with its low half, so the wire key would change. Signed-off-by: Cosmin Staicu <cosmin.staicu@uipath.com>
e332441 to
160611f
Compare
|
|
|
||
| namespace UiPath.Caching.Telemetry; | ||
|
|
||
| /// <summary>Times one cache operation and reports it; a struct, so an operation allocates nothing for its telemetry.</summary> |
|
|
||
| private readonly Target? _target; | ||
| private readonly long _startTimestamp; | ||
| private readonly long _startUtcTicks; |






Follows #214, now merged; the base is
main.This is the breaking follow-up to #214, for the unreleased major. It removes the per-operation allocations #214 could not reach without changing the API, in four changes:
PrefixRedisKeyStrategybuilt a new string for every key. It now encodes its prefix once and returns aRedisKeycarrying that byte prefix next to the caller's name.ConnectorKeyPrefixkeeps the bytes it reads.Prefix/Separatorsetters are removed, and a surrogateseparatoris refused: encoded apart from the name it could no longer pair with a name that starts with its low half.IResiliencePipeline.ExecuteAsync<TResult, TState>hands a state value to a static callback. The caches and topics use it, so a command no longer allocates a closure and a delegate.Utf8JsonWriterinto anArrayPoolbuffer, which the cache returns after the write. The pooled payload type is internal, so no caller outside the caches can return a buffer twice.Serialize: custom options and runtime-type contracts are covered by tests. A subclass of the JSON proxy, like any other serializer, keeps lending its own memory.TelemetryOperation.TelemetryScopereplaces it: a struct timed by the cache'sTimeProviderthat reports through the provider'sTrackMetric/TrackDependencywith the same names and tags.ITelemetryOperation,TelemetryOperation,NullTelemetryOperationandICachingTelemetryProvider.StartOperationare removed.TelemetryScopeand the pooled writer live inUiPath.Caching. The abstractions package keeps only interfaces, null implementations and DTOs.One follow-up from the #214 review:
RedisCacheBase.SupportsExpireTimeresolves the server version on first use instead of in the constructor, so constructing a cache that never reads an expiration, such asRedisSetCache, no longer connects. A test pins that construction does not touchVersion.Benchmark
A single-operation BenchmarkDotNet run with
MemoryDiagnoseragainst a local Redis on net10.0. The telemetry-on column uses the OpenTelemetry provider, with aMeterListenersubscribed the way an SDK would. Each row is the bytes allocated per call after that change. Time is dominated by the Redis round trip, so allocations are the signal.SetAsyncis left out. It also counts allocations from the background stream reader and varies by more than these steps between runs.Migration
TrackMetric/TrackDependency/TrackEvent/TrackExceptionneeds no change.StartOperationtimes an operation withnew TelemetryScope(provider, timeProvider, providerName, method, type), callingStop()andTrack(hit, keyCount). The tag constants moved fromTelemetryOperationtoTelemetryScope, which is in theUiPath.Cachingpackage.PrefixRedisKeyStrategythat assignedPrefixorSeparatorpasses them to the constructor instead.Verification
dotnet build -c Release -warnaserrordotnet test: 4071 passed on net8.0 and net10.0, with the Redis integration tests on.