Skip to content

Memoize SafetyEvaluator type evaluations - #2976

Open
helenyugithub wants to merge 3 commits into
palantir:developfrom
helenyugithub:safety-evaluator-memoization
Open

Memoize SafetyEvaluator type evaluations#2976
helenyugithub wants to merge 3 commits into
palantir:developfrom
helenyugithub:safety-evaluator-memoization

Conversation

@helenyugithub

Copy link
Copy Markdown

Before this PR

SafetyEvaluator re-evaluates the transitive log-safety of a referenced type at every reference site: FieldSafetyVisitor#visitReference descends into the referenced type's full definition each time, so evaluation cost is proportional to the number of paths through the type graph rather than the number of types. On large, dense conjure definitions this dominates codegen wall-clock.

Measured on real-world IRs (conjure-java 8.70.0, objects flavor, single run each):

IR generated files before after
856KB IR (union-heavy chart definitions) 881 248s 10s
3.2MB IR 2371 353s 35s

During the slow runs, 20/20 thread-dump samples were inside the SafetyEvaluator recursion. Generated output with the cache is byte-identical on both IRs (318/318 and 2371/2371 files).

After this PR

Safety results are memoized per TypeName in a cache scoped to the SafetyEvaluator instance, shared across the visitor instances it creates. The cache respects the recursive-type cycle guard: values computed after hitting the inProgress set embed the SAFE cycle substitution and are context-dependent, so only results computed from a cycle-free subtree — or at the outermost evaluation of a type, which is canonical by definition — are cached.

Two tests added: repeated evaluations on a shared evaluator agree with fresh evaluators, and a Foo ↔ Bar cycle where only Foo carries unsafe data verifies that Bar's within-cycle value (SAFE by substitution) is not reused for its standalone evaluation (UNSAFE).

Possible downsides?

The cache retains one entry per named type for the lifetime of the SafetyEvaluator — negligible next to the ConjureDefinition it already holds. SafetyEvaluator was not thread-safe before this change (callers construct per-generation instances) and remains so.

SafetyEvaluator re-evaluates the transitive safety of a referenced type at
every reference site, so evaluation cost is proportional to the number of
paths through the type graph rather than its size. On large, dense conjure
definitions this dominates java codegen: a real-world 3.2MB IR spends ~350s
in objects generation, with all sampled stacks inside SafetyEvaluator; the
same IR generates in ~35s with this cache, with byte-identical output.

Results are memoized per TypeName, respecting the recursive-type cycle
guard: values computed after hitting the in-progress set are substituted
with SAFE and therefore context-dependent, so only cycle-free subtree
results and outermost (canonical) evaluations enter the cache.
@changelog-app

changelog-app Bot commented Jul 14, 2026

Copy link
Copy Markdown

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

Memoize SafetyEvaluator type evaluations

Check the box to generate changelog(s)

  • Generate changelog entry

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.

1 participant