Skip to content

Deduplicate TypeMarker, ExceptionDeserializerArgs, and Deserializer fields in generated dialogue interfaces#2869

Open
schlosna wants to merge 4 commits intodevelopfrom
davids/reateExceptionDeserializerArgs
Open

Deduplicate TypeMarker, ExceptionDeserializerArgs, and Deserializer fields in generated dialogue interfaces#2869
schlosna wants to merge 4 commits intodevelopfrom
davids/reateExceptionDeserializerArgs

Conversation

@schlosna
Copy link
Copy Markdown
Contributor

@schlosna schlosna commented Apr 10, 2026

Before this PR

Generated dialogue service interfaces previously created per-endpoint TypeMarker
anonymous classes, ExceptionDeserializerArgs instances, and Deserializer fields,
causing excessive allocations when multiple endpoints share the same return type.

This adds unnecessary overhead for conjure clients that use sticky channels such as ⛰️ clients via e.g. DataServiceAsync#of(com.palantir.dialogue.EndpointChannelFactory, com.palantir.dialogue.ConjureRuntime).

After this PR

==COMMIT_MSG==
This change modifies DefaultStaticFactoryMethodGenerator to:

  1. Generate one private static final TypeMarker<T> per distinct return type
    instead of new TypeMarker<T>() {} inline per endpoint.
  2. Generate one private static final ExceptionDeserializerArgs<T> per distinct
    return type (error-respecting path), instead of calling
    createExceptionDeserializerArgs per endpoint.
  3. Generate one shared Deserializer<T> instance field per distinct return type,
    referenced by all endpoints with the same return type.

For example EteServiceBlocking (36 endpoints, 22 distinct return types) this reduces
anonymous TypeMarker classes, createExceptionDeserializerArgs calls, and
Deserializer allocations from 36 each to 22 each.

==COMMIT_MSG==

Possible downsides?

@changelog-app
Copy link
Copy Markdown

changelog-app bot commented Apr 10, 2026

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

This change modifies DefaultStaticFactoryMethodGenerator to:

  1. Generate one private static final TypeMarker<T> per distinct return type
    instead of new TypeMarker<T>() {} inline per endpoint.
  2. Generate one private static final ExceptionDeserializerArgs<T> per distinct
    return type (error-respecting path), instead of calling
    createExceptionDeserializerArgs per endpoint.
  3. Generate one shared Deserializer<T> instance field per distinct return type,
    referenced by all endpoints with the same return type.

For EteServiceBlocking (36 endpoints, 22 distinct return types) this reduces
anonymous TypeMarker classes, createExceptionDeserializerArgs calls, and
Deserializer allocations from 36 each to 22 each.

Check the box to generate changelog(s)

  • Generate changelog entry

…ields in generated dialogue interfaces

Generated dialogue service interfaces previously created per-endpoint TypeMarker
anonymous classes, ExceptionDeserializerArgs instances, and Deserializer fields,
causing excessive allocations when multiple endpoints share the same return type.

This change modifies DefaultStaticFactoryMethodGenerator to:

1. Generate one `private static final TypeMarker<T>` per distinct return type
   instead of `new TypeMarker<T>() {}` inline per endpoint.
2. Generate one `private static final ExceptionDeserializerArgs<T>` per distinct
   return type (error-respecting path), instead of calling
   createExceptionDeserializerArgs per endpoint.
3. Generate one shared `Deserializer<T>` instance field per distinct return type,
   referenced by all endpoints with the same return type.

For EteServiceBlocking (36 endpoints, 22 distinct return types) this reduces
anonymous TypeMarker classes, createExceptionDeserializerArgs calls, and
Deserializer allocations from 36 each to 22 each.
…enerate()

Refactored the generate() method by extracting logic into smaller, focused methods to improve maintainability and reduce complexity.
…ersion, and deterministic hashing for deconfliction

- Convert ReturnTypeFieldsContext from class to record for conciseness
- Use Guava CaseFormat for converting type names instead of manual character manipulation
- Use deterministic Guava Hashing (murmur3_32_fixed) with base-36 encoding for deconflicting field names instead of sequential numeric suffixes
- Regenerate integration test expected files to reflect the new field naming
@schlosna schlosna force-pushed the davids/reateExceptionDeserializerArgs branch from 556ec6a to f1bea45 Compare April 10, 2026 19:55
@changelog-app
Copy link
Copy Markdown

changelog-app bot commented Apr 10, 2026

Successfully generated changelog entry!

Need to regenerate?

Simply interact with the changelog bot comment again to regenerate these entries.


📋Changelog Preview

💡 Improvements

  • This change modifies DefaultStaticFactoryMethodGenerator to:

    1. Generate one private static final TypeMarker<T> per distinct return type
      instead of new TypeMarker<T>() {} inline per endpoint.
    2. Generate one private static final ExceptionDeserializerArgs<T> per distinct
      return type (error-respecting path), instead of calling
      createExceptionDeserializerArgs per endpoint.
    3. Generate one shared Deserializer<T> instance field per distinct return type,
      referenced by all endpoints with the same return type.

    For EteServiceBlocking (36 endpoints, 22 distinct return types) this reduces
    anonymous TypeMarker classes, createExceptionDeserializerArgs calls, and
    Deserializer allocations from 36 each to 22 each. (#2869)

@schlosna schlosna requested review from a team, bjlaub and mpritham and removed request for a team April 10, 2026 22:51
@schlosna schlosna marked this pull request as ready for review April 10, 2026 22:51
/** Creates an asynchronous/non-blocking client for a EmptyPathService service. */
static EmptyPathServiceAsync of(EndpointChannelFactory _endpointChannelFactory, ConjureRuntime _runtime) {
return new EmptyPathServiceAsync() {
private static final TypeMarker<Boolean> booleanTypeMarker = new TypeMarker<Boolean>() {};
Copy link
Copy Markdown
Member

@pkoenig10 pkoenig10 Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably would prefer if these weren't static. It means that simply loading these classes will requiring initializing and retaining these classes, even if the client is no longer used.

@mpritham
Copy link
Copy Markdown
Contributor

Discussed internally that this proposed Dialogue change is probably the more useful change to mitigate the overhead from using sticky clients. Hopefully that change will take the construction of these clients out of a hot path.

I think this is a good idea though. I think the hashes added to the variable names when there are name collisions hurts the readability: I wonder if there's another way to deconflict here.

@schlosna
Copy link
Copy Markdown
Contributor Author

Discussed internally that this proposed Dialogue change is probably the more useful change to mitigate the overhead from using sticky clients. Hopefully that change will take the construction of these clients out of a hot path.

Fixed up merge conflicts and finished out implementation of #2647 in palantir/dialogue#2955 on top of that branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants