perf(redis): keep JSON serialization on UTF-8 bytes instead of UTF-16 strings - #88
Merged
Conversation
cosmin-staicu
requested review from
alinahornet,
cosminvlad,
litheon,
lucianaparaschivei and
razvalex
as code owners
July 17, 2026 08:50
cosmin-staicu
force-pushed
the
chore/stackexchange-redis-3
branch
from
July 17, 2026 09:26
0145220 to
d103e81
Compare
cosmin-staicu
force-pushed
the
perf/utf8-serializer
branch
from
July 17, 2026 09:36
8e0cfcc to
0a98e9f
Compare
… strings JSON is UTF-8 on the wire, so round-tripping cache payloads through a UTF-16 string only added an allocation + transcode on every read and write. This moves the serialization boundaries to bytes (enabled by StackExchange.Redis 3.0's RedisValue <-> ReadOnlyMemory/ReadOnlySequence<byte> conversions). - SystemJsonSerializerProxy: serialize via SerializeToUtf8Bytes; deserialize from the RedisValue payload as ReadOnlySpan<byte>. Also deserialize a JsonElement directly instead of via GetRawText(). Shared by RedisCache, RedisHashCache, RedisSetCache, RedisCacheProvider, so all inherit the win. - Broadcast: CacheEventFormatter.Encode uses SerializeToUtf8Bytes; the Redis publish paths (RedisPubSubTopic, RedisStreamsTopic) publish the encoded bytes as a RedisValue directly; the subscribe paths (RedisPubSubSubjectWriter, RedisStreamSubjectWriter) decode from the RedisValue bytes. - Deprecate the now-unused string helpers IEventFormatterProxy<T>.Decode(string) and EncodeAsString(T); migrate the stream-subject-writer tests to the ReadOnlyMemory<byte> overload. Wire format is unchanged everywhere, so existing cached/published data still deserializes. No public API change beyond the two [Obsolete] markers. Measured with a new SerializerBenchmark ([MemoryDiagnoser]): ~2x fewer allocations on serialize/deserialize (e.g. large-payload deserialize 1,385,629 B -> 607,714 B; serialize 778,689 B -> 389,660 B) with equal-or-better throughput. The benchmark also compared a ReadOnlySequence<byte>/Utf8JsonReader read variant; it allocated the same as the span path but ran slower on these single-segment payloads, so the span path was chosen. Full suite green on net8.0 and net10.0 (1178/1178 each). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Cosmin Staicu <cosmin.staicu@uipath.com>
cosmin-staicu
force-pushed
the
perf/utf8-serializer
branch
from
July 17, 2026 09:38
0a98e9f to
1dbe334
Compare
|
alinahornet
approved these changes
Jul 17, 2026
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.



Summary
JSON is UTF-8 on the wire, but the cache serializer round-tripped every payload through a UTF-16
string, adding an allocation + transcode on every read and write. This keeps serialization on UTF-8 bytes end-to-end, enabled by StackExchange.Redis 3.0'sRedisValue↔ReadOnlyMemory/ReadOnlySequence<byte>conversions. No public API change beyond two[Obsolete]markers; wire format is unchanged so existing cached/published data still deserializes.Changes
SystemJsonSerializerProxy(the default proxy, shared byRedisCache,RedisHashCache,RedisSetCache,RedisCacheProvider— all inherit the win): serialize viaSerializeToUtf8Bytes; deserialize from theRedisValuepayload asReadOnlySpan<byte>; deserialize aJsonElementdirectly instead of viaGetRawText().CacheEventFormatter.EncodeusesSerializeToUtf8Bytes; the publish paths (RedisPubSubTopic,RedisStreamsTopic) publish the encoded bytes as aRedisValuedirectly; the subscribe paths (RedisPubSubSubjectWriter,RedisStreamSubjectWriter) decode from theRedisValuebytes.IEventFormatterProxy<T>.Decode(string)andEncodeAsString(T)(now unused by the library — they forced a UTF-16 transcode). Stream-subject-writer tests migrated to theReadOnlyMemory<byte>overload.Benchmark
New
SerializerBenchmark([MemoryDiagnoser]). Allocation numbers are exact/reliable; roughly halved on both paths:The benchmark also measured a
ReadOnlySequence<byte>/Utf8JsonReaderread variant; it allocated identically to the span path but ran slower on these single-segment payloads, so the span path was chosen. (Run:dotnet run -c Release --framework net10.0 --project benchmarks/UiPath.Caching.Benchmarks -- --filter *SerializerBenchmark*)Test plan
SystemJsonSerializerProxyTestsincl. legacy string-path round-trip; broadcast tests migrated)dotnet test)Full suite green on net8.0 and net10.0 — 1178/1178 each — with
RUN_REDIS_INTEGRATION_TESTS=1against a live Redis.Linked issues
Fixes #
Contributor declaration
git commit -s).🤖 Generated with Claude Code