Skip to content

#3582 [Performance] Avoid repeating per-class work while processing annotated types - #3583

Merged
manovotn merged 1 commit into
weld:mainfrom
flowlogix:with-annotations-filter-perf
Nov 3, 2026
Merged

manovotn merged 1 commit into
weld:mainfrom
flowlogix:with-annotations-filter-perf

Conversation

@lprimak

@lprimak lprimak commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3582.

Weld checked each class by asking small questions one at a time, and every question walked through the class again. Now the class is walked once and the questions are answered from that result. The results are the same.

Used in two places:

  • RequiredAnnotationDiscovery: the annotation types found in a class (plus those on its annotations, one level deep, as before) are kept in a set per class, instead of searching the class again for each annotation of each @WithAnnotations observer. The set is dropped in cleanupAfterBoot().
  • EnhancedAnnotatedTypeImpl#getOverriddenMethods: methods are grouped by declaring class once, in the same order as before, instead of going through all methods for every class in the hierarchy.

In a Payara 7 redeploy of a PrimeFaces application this took the time from about 550 ms to about 320-345 ms (details in #3582).

@lprimak
lprimak requested a review from manovotn as a code owner October 29, 2026 01:41
@lprimak lprimak changed the title #3582 Scan a class once when resolving @WithAnnotations observers #3582 Scan a class only once when resolving @WithAnnotations observers Oct 29, 2026
@lprimak
lprimak force-pushed the with-annotations-filter-perf branch from 2feae9d to 98f5fcb Compare October 29, 2026 01:49
@lprimak
lprimak force-pushed the with-annotations-filter-perf branch from 98f5fcb to e990719 Compare October 29, 2026 02:22
@lprimak lprimak changed the title #3582 Scan a class only once when resolving @WithAnnotations observers #3582 Avoid repeating per-class work while processing annotated types Oct 29, 2026

@manovotn manovotn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I did leave some comments and I am still looking into this but there are few bigger areas that we should clear up first.

Firstly, Weld has a small repo with some benchmarks, it might be worth adding something there and measure it as that would be stripped of any influence of a whole EE server - https://github.com/weld/weld-core-benchmarks. While I appreciate your findings, I know from my own experience with WFLY that the server overhead can significantly temper with results

  • We should also consider measuring how much more expensive your solution gets for some simple cases. While it improves large apps, I guess it will actually hinder smaller deployments by searching the whole deployment where previously we could get a quick answer from top level class.

Secondly, many of the tests you have here overlap with what Weld already has. I think it might be smarter to instead expand on existing test coverage (or add tests into those places).

  • For annotation discovery, there is WithAnnotationsTest (note that there is SE variant and another in tests-arquillian) which I think should be compared to your coverage and extended.
  • For overriden methods, this is a bit more convoluted - here I agree your test is probably better provided you remove the old algorithm and stick to asserting expected results.

Lastly, there is one case that your approach will fail at - current code uses a short-circuit approach where it returns the first possible result and hence can bypass what would otherwise result in an error. Here's the code:

class Base {
    public Base() {}
    private Base(Missing optionalDependency) {}
}

@Dependent
@Marker
class Child extends Base {}

public class VetoChildExtension implements Extension {
    void vetoChild(
            @Observes @WithAnnotations(Marker.class)
            ProcessAnnotatedType<Child> event) {
        event.veto();
    }
}

Missing is available during compilation but absent at runtime.
In current Weld code, this works and deploys whereas in your case, it will blow up as you attempt to read the whole complete picture and encounter Missing in there.

@manovotn

Copy link
Copy Markdown
Member

@lprimak actually, seeing the PR, I was thinking I we could instead populate the cache incrementally while searching.
Now, this might be overly complex and not worth doing but in theory or would solve the short-circuit case while helping repeated searches.

We should consider:

  • If it even makes sense perf. wise - i.e. the benchmarks for smaller apps I mentioned might justify such change
  • Is the short-circuit approach worth keeping? I would say in many cases it might well be sufficient.
  • If it's doable - does this part of bootstrap happen synchronously or in parallel? If it's the latter, then we couldn't do it for sure.

However, a complete incrementally populated cache might be an overkill.
Alternatively, we could also consider partial caching:

  1. Keep the original short-circuit search.
  2. Collect encountered annotation types while searching.
  3. If a match is found early, return immediately.
  4. If the search reaches the end, publish the complete collected set for subsequent queries.

That avoids storing resumable traversal state. One negative query produces a complete cache, benefiting later queries. Classes that repeatedly match early retain their existing cheap behavior. The limitation is that repeated successful searches near the end may still repeat work until a search completes.

Thoughts?
Also, let's see if we could summon @mkouba here and get his 0.02$ on this :)

@manovotn

Copy link
Copy Markdown
Member

However, a complete incrementally populated cache might be an overkill.
Alternatively, we could also consider partial caching:

FTR, I am trying to get AI to create some first draft of those to see if it's even sensible; will update this PR once I get some form of result which may or may not be tomorrow based on my other work.

@lprimak

lprimak commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi, Matej, thank you for the review, as this is a big performance improvement, I appreciate your efforts helping to get this merged.

I wanted to make this change as simple and DRY as possible, to minimize regressions, thus came up with the simplest solution to the problem that profiler was showing.
I think that incremental cache is indeed an overkill here.

Smaller apps: While I agree with your assessment here, does this matter holistically? This kind of deploy performance is measured in wall clock, so for small apps, even if this optimization is less efficient, would it not actually matter?

FYI: I had AI write the tests and didn't pay too much attention to them, so they may very well be mostly AI slop and need improvement.
Now, I paid a bit more attention to the tests :)

@lprimak

lprimak commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

@manovotn Implemented short-circuit, was not a huge change

@manovotn manovotn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Added few more small remarks.

I have yet to re-review those tests (and likely won't be able to until early next week) but otherwise I think the optimization like this makes sense. The short-circuit is IMO worth keeping.

@lprimak
lprimak requested a review from manovotn October 30, 2026 16:33
@lprimak

lprimak commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

All your review comments make sense and have been applied - thank you!

@lprimak lprimak changed the title #3582 Avoid repeating per-class work while processing annotated types #3582 [Performance] Avoid repeating per-class work while processing annotated types Nov 1, 2026
@manovotn

manovotn commented Nov 3, 2026

Copy link
Copy Markdown
Member

@lprimak please squash the commits into a single one with some reasonable message and do so on both branches.
Other than that, I think it's good to go.

- RequiredAnnotationDiscovery searched the whole class hierarchy once for
  each annotation that a @WithAnnotations observer asks for. A class is now
  searched once, and the result is kept until bootstrap is finished.
- EnhancedAnnotatedTypeImpl#getOverriddenMethods went through all methods of
  a type again for every class in its hierarchy. The methods are now grouped
  by their declaring class once.

The results are the same as before. This is an optimization.

Signed-off-by: lprimak <lenny@flowlogix.com>
@lprimak
lprimak force-pushed the with-annotations-filter-perf branch from 29ffd1e to 70f7064 Compare November 3, 2026 18:49
@lprimak

lprimak commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

please squash the commits

Done for both branches.
FYI: GitHub "Squash and Merge" can be used to do the same thing, without forcing force-pushes, which could be destructive if not careful.

@manovotn manovotn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Thank you for contributing @lprimak!

I assume we could have a release by the end of the month (for both versions). In case the date is nearing and I still haven't done it, don't hesitate to poke me here 😅

@manovotn
manovotn merged commit b09c5ba into weld:main Nov 3, 2026
17 checks passed
@lprimak
lprimak deleted the with-annotations-filter-perf branch November 3, 2026 20:04
@lprimak

lprimak commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Awesome. Glad to contribute again, 10 years later :)

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.

[Performance] Weld repeats the same work for every class

2 participants