Skip to content

Spike: Parallelize ResolverChain resolution while preserving circular-reference detection #60

Closed

Description

@MessiasLima

Description

Determine whether (and how) ResolverChain.resolve(...) can internally parallelize recursive type resolution without breaking circular-reference detection and without exposing suspend in the public API.

Background

ResolverChain is the central orchestrator of type resolution. Today it is strictly sequential. For deep or wide object graphs (e.g. a class with 20 constructor parameters, or a List<ComplexType> of size 100), the sequential chain.resolve(...) calls are the bottleneck. Because each resolution is CPU-bound and independent of its siblings, they are ideal candidates for parallel execution.

Hard constraints:

  • No suspend in the public API. fun resolve(type: KType): Any? and fun TypeResolver.resolve(type: KType, chain: ResolverChain): Any? must not become suspend functions. Users call some<T>() from plain Kotlin/Java code.
  • Circular-reference detection must stay exact. The current stack semantics (classifier equality + nullable/non-nullable rules) must continue to detect cycles such as data class Node(val next: Node?) and throw SomeCircularReferenceException when appropriate.
  • No behavioural changes for sequential callers. If the library is used on a single thread, the outcome must be identical to today.

Current Knowledge

  • ResolverChain maintains a single mutableListOf<KType>() (resolutionStack) to detect circular references. This shared mutable state is not thread-safe and will be corrupted if sibling resolutions run in parallel.
  • The main parallelism candidates are ClassResolver (constructor parameters) and collection resolvers (ListResolver, SetResolver, ArrayResolver, MapResolver) because their child resolutions are independent.
  • Resolvers with single recursive calls (NullableResolver, SealedClassResolver, ValueClassResolver) will not benefit from parallelism.
  • Kotlin coroutines can be hidden behind a synchronous API via runBlocking { ... }, but the overhead and thread-pool implications are unknown.
  • Alternative concurrency mechanisms (e.g. ExecutorService, ForkJoinPool) have not been evaluated.
  • There is no existing benchmark harness for comparing sequential vs parallel resolution.

Scope

In scope:

  • Investigating how to remove or make safe the shared resolutionStack state.
  • Evaluating whether to introduce parallelism transparently or via an opt-in API (e.g. chain.resolveAll(List<KType>): List<Any?>).
  • Comparing coroutines (runBlocking + Dispatchers.Default) vs ExecutorService / ForkJoinPool for hidden concurrency.
  • Benchmarking sequential vs parallel for small, wide, and deep graphs.
  • Documenting the recommended approach and, if viable, a task breakdown for implementation.

Out of scope:

  • Actually implementing the parallelization in production code.
  • Adding new public API surface (the spike may recommend one, but designing it is part of the follow-up).
  • Changes to resolver generation strategies or randomization behaviour.

Outcomes

  • Identify a thread-safe replacement for the shared resolutionStack that preserves exact circular-reference semantics.
  • Confirm whether parallelism should be transparent or opt-in (e.g. resolveAll API).
  • Compare coroutines-under-runBlocking vs ExecutorService / ForkJoinPool in terms of complexity, overhead, and compatibility with existing callers.
  • Benchmark sequential vs parallel for shallow, wide, and deep object graphs; measure latency and memory impact.
  • Produce a design document (markdown) with a clear recommendation: go / no-go / go with opt-in flag.
  • If the recommendation is "go", produce a rough task breakdown for the implementation ticket(s).

Activity

  1. added this to the next milestone on Sep 17, 2026
  2. moved this from Ready to In progress in Someon Sep 17, 2026
  3. MessiasLima commented on Sep 18, 2026

    @MessiasLima
    OwnerAuthor

    Investigation result: parallelization is a no-go; caching is the better path

    We investigated parallelizing ResolverChain.resolve(...) for this spike. Conclusion: parallelization is not a good solution for improving performance here. The main costs and blockers:

    • The shared resolutionStack is central to circular-reference detection. Parallel branches would require a thread-safe redesign plus per-branch context propagation, adding complexity and risk to the exact cycle semantics.
    • Parallel dispatch overhead (dispatcher/thread-pool fan-out and coordination) outweighs the benefit for the small per-resolution work units, which are dominated by cheap reflection/metadata lookups rather than heavy compute.
    • Resolvers and the random source are shared, so parallel sibling resolution would need extra isolation to keep generation deterministic.

    Instead, caching is the best solution. The expensive parts are repeated reflection/metadata lookups that are stable for a given class or type, so caching removes repeated work without touching resolution semantics or introducing concurrency.

    Caching tasks created

    Supporting changes

    Recommendation: no-go on parallelization; pursue the caching tasks above instead.

  4. moved this from In progress to Done in Someon Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    • Status
      Done

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions