Skip to content

Reduce allocations on event/request hot paths#50

Draft
Copilot wants to merge 2 commits into
masterfrom
copilot/improve-code-performance
Draft

Reduce allocations on event/request hot paths#50
Copilot wants to merge 2 commits into
masterfrom
copilot/improve-code-performance

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

Profiling-driven cleanup of the publish/dispatch hot paths in RedisEventBusImpl and RequestResponseBusImpl. Wire format and public API are unchanged.

Streaming envelope serialization

Publish previously did encodeToJsonElement (full JsonElement tree allocation) followed by encodeToString (re-walks the tree). Replaced with encode-only KSerializers that stream the typed payload straight into the output:

@OptIn(ExperimentalSerializationApi::class)
private class EventEnvelopeSerializer(
    private val dataSerializer: KSerializer<RedisEvent>
) : KSerializer<EventEnvelopePayload> {
    override val descriptor = buildClassSerialDescriptor("EventEnvelope") {
        element<String>("eventClass")
        element("eventData", dataSerializer.descriptor)
    }
    override fun serialize(encoder: Encoder, value: EventEnvelopePayload) {
        encoder.encodeStructure(descriptor) {
            encodeStringElement(descriptor, 0, value.eventClass)
            encodeSerializableElement(descriptor, 1, dataSerializer, value.event)
        }
    }
    override fun deserialize(decoder: Decoder) = error("encode-only")
}

Element names match the existing data-class envelopes so JsonNamingStrategy.SnakeCase produces identical bytes. The receive path still uses the data-class envelopes (concrete payload type unknown until eventClass is read).

Other changes

  • Single dispatch coroutine per message. handleIncomingMessage now launches one coroutine that iterates handlers, instead of one per handler. Per-handler exception isolation preserved via try/catch.
  • Skip redundant responseTypeRegistry writes. containsKey short-circuits the per-request putIfAbsent once the response type has been seen.
  • Coalesced registrationLock.write blocks in both buses — one write per handler, no inconsistent intermediate state.
  • Negative-result caching in KotlinSerializerCache. Added a MISSING sentinel inside the ClassValue so classes without a serializer aren't re-resolved on every lookup.
  • Removed the now-unused serializeEvent / serializeRequest / serializeResponse helpers and the EventEnvelope.forEvent / RequestEnvelope.forRequest / ResponseEnvelope.forResponse factories.

Not done

  • (2) wire-format change to a delimited form, (4) processStreamEvent split avoidance, (9) Flux.interval polling, (10) counted-loop iteration — out of scope for the recommended subset.
  • (8) LuaScriptExecutor cached-SHA fast-path — Aedile's getIfPresent is suspend and .underlying() is parameterized over Deferred<V>, not V. Left as-is pending a way to verify against a working build.

Build note

The repo's settings plugin (dev.slne.surf.api.gradle.settings, hosted on repo.slne.dev / reposilite.slne.dev) isn't reachable from this sandbox, so changes were not compiled here.

Copilot AI and others added 2 commits April 30, 2026 22:49
Copilot AI review requested due to automatic review settings April 30, 2026 22:52
Copilot AI review requested due to automatic review settings April 30, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants