Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Excessive locking in TypeCachingBytecodeGenerator#BOOTSTRAP_LOCK #3095

Merged
merged 1 commit into from Aug 22, 2023

Conversation

AndreasTu
Copy link
Contributor

@AndreasTu AndreasTu commented Aug 17, 2023

Changed locking scheme in TypeCachingBytecodeGenerator from a single global lock to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on the hashcode of MockitoMockKey, which aggregates the types and
settings to mock.

The old global BOOTSTRAP_LOCK did block Threads regardless, if they try to mock the same type or not.

Fixes #3035

Checklist

  • Read the contributing guide
  • PR should be motivated, i.e. what does it fix, why, and if relevant how
  • If possible / relevant include an example in the description, that could help all readers
    including project members to get a better picture of the change
  • Avoid other runtime dependencies
  • Meaningful commit history ; intention is important please rebase your commit history so that each
    commit is meaningful and help the people that will explore a change in 2 years
  • The pull request follows coding style
  • Mention Fixes #<issue number> in the description if relevant
  • At least one commit should mention Fixes #<issue number> if relevant

@TimvdLippe
Copy link
Contributor

I know that the MockitoKey is finicky and that we previously broke the caching and nuked performance with this. @raphw Can you please look at this change and verify we don't reintroduce that behavior? I also remember us talking about writing a regression test for this and I don't think we ever did that.

@codecov-commenter
Copy link

codecov-commenter commented Aug 17, 2023

Codecov Report

Patch coverage: 100.00% and project coverage change: +73.06% 🎉

Comparison is base (076e8ac) 12.39% compared to head (6d49851) 85.45%.

Additional details and impacted files
@@              Coverage Diff              @@
##               main    #3095       +/-   ##
=============================================
+ Coverage     12.39%   85.45%   +73.06%     
- Complexity      422     2889     +2467     
=============================================
  Files           329      329               
  Lines          8813     8822        +9     
  Branches       1094     1095        +1     
=============================================
+ Hits           1092     7539     +6447     
+ Misses         7510      995     -6515     
- Partials        211      288       +77     
Files Changed Coverage Δ
...eation/bytebuddy/TypeCachingBytecodeGenerator.java 81.13% <100.00%> (+35.67%) ⬆️

... and 299 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@TimvdLippe
Copy link
Contributor

For reference, this was the PR that reverted the bad patch: #2402

@raphw
Copy link
Member

raphw commented Aug 17, 2023

I think the issue would be if somebody created a mock for interface A with additional interface B while another thread created a mock for interface B with additional interface A, or with different settings. I mildly remember that the bug came from this corner.

As far as I recall, the lock is only hold if the mock does not yet exist? We concluded that the costs of mocking are much higher than the costs of locking, so it would not be a problem.

If we can improve this, I welcome it, but I think the key must be broader to cover the mentioned scenarios.

@AndreasTu
Copy link
Contributor Author

Do I understand that correctly, that the required lock to hold must cover the mockType and the interfaces or the whole MockitoMockKey?

@raphw
Copy link
Member

raphw commented Aug 19, 2023

Yes, depending on the settings Mockito needs to create multiple mock types for the same base type.

@AndreasTu
Copy link
Contributor Author

@raphw I have changed the locking behavior to account for the data in the MockitoMockKey.
Can you have a look, if you have time?

@raphw
Copy link
Member

raphw commented Aug 19, 2023

That's a good idea. We should however add a test that stresstests this, one with a purpuseful hash collision, and one without.

Changed locking scheme in TypeCachingBytecodeGenerator from a
single global lock to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of MockitoMockKey, which aggregates the types and
settings to mock.

Fixes mockito#3035
@AndreasTu
Copy link
Contributor Author

@raphw done.

@raphw
Copy link
Member

raphw commented Aug 19, 2023

Looks good to me!

@raphw raphw self-requested a review August 19, 2023 22:05
Copy link
Contributor

@TimvdLippe TimvdLippe left a comment

Choose a reason for hiding this comment

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

Good solution! Did you locally verify that your test fails if you revert your fix? And did you rerun the test say 10 times to ensure it is not flaky?

@AndreasTu
Copy link
Contributor Author

@TimvdLippe The tests are not failing if I revert my change, because the old code was also correct, just more blocking.
But I can see the lock contention in a FlightRecorder measurement (see below).
I have executed it at least 10 times.

Flight recorder measurement.
Left side after fix, right side before the fix:

Lock_Fix

The stacktrace of the image, is the stack of the right side.
The remaining Object Lock on the left side is (ClassLoading, which is to expect):

jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(String, boolean)	
jdk.internal.loader.BuiltinClassLoader.loadClass(String, boolean)	
jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(String, boolean)	
java.lang.ClassLoader.loadClass(String)	
void org.mockito.internal.creation.bytebuddy.TypeCachingMockBytecodeGeneratorTest.assertValidMockClass(MockFeatures, Class)

@AndreasTu
Copy link
Contributor Author

@TimvdLippe Out of cusiosity, how about I open another PR to integrate Java FlightRecorder measurments for tests if you specify the flag -Pjfr for a test execution, like gradlew :test -Pjfr?
Because it is cumbersome to create such a measurement, when you have to copy all the arguments and files paths into Gradle for each time you need it.

@TimvdLippe
Copy link
Contributor

Yes that sounds like a great idea. If this test doesn't fail, I think it makes sense if we create a new subproject called performance-test. There we put tests like these, which we should compile, but don't run (since they are compute-intensive). Then when you specify that argument, we do run the tests and start analysis. Great idea!

@AndreasTu
Copy link
Contributor Author

It would fail, if the locking is somehow broken. And the test are not that slow on my machine. So I would say these still make sense to always execute them.

@TimvdLippe
Copy link
Contributor

That works for me! Looking forward to your next PR to integration the JFR monitoring as well

@TimvdLippe TimvdLippe merged commit 741fe81 into mockito:main Aug 22, 2023
13 checks passed
@AndreasTu AndreasTu deleted the fix_3035 branch August 22, 2023 06:55
AndreasTu added a commit to AndreasTu/spock that referenced this pull request Sep 3, 2023
Changed locking scheme in ByteBuddyMockFactory from a
single global lock CACHE to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of TypeCache.SimpleKey, which aggregates the types and
settings to mock.

The old global CACHE lock did block Threads regardless,
if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095
AndreasTu added a commit to AndreasTu/spock that referenced this pull request Sep 3, 2023
Changed locking scheme in ByteBuddyMockFactory from a
single global lock CACHE to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of TypeCache.SimpleKey, which aggregates the types and
settings to mock.

The old global CACHE lock did block Threads regardless,
if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095
AndreasTu added a commit to AndreasTu/spock that referenced this pull request Sep 3, 2023
Changed locking scheme in ByteBuddyMockFactory from a
single global lock CACHE to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of TypeCache.SimpleKey, which aggregates the types and
settings to mock.

The old global CACHE lock did block Threads regardless,
if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095
AndreasTu added a commit to AndreasTu/spock that referenced this pull request Sep 3, 2023
Changed locking scheme in ByteBuddyMockFactory from a
single global lock CACHE to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of TypeCache.SimpleKey, which aggregates the types and
settings to mock.

The old global CACHE lock did block Threads regardless,
if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095
AndreasTu added a commit to AndreasTu/spock that referenced this pull request Sep 4, 2023
Changed locking scheme in ByteBuddyMockFactory from a
single global lock CACHE to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of TypeCache.SimpleKey, which aggregates the types to mock.

The old global CACHE lock did block Threads regardless,
if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095

Co-authored-by: Björn Kautler <Bjoern@Kautler.net>
AndreasTu added a commit to AndreasTu/spock that referenced this pull request Sep 6, 2023
Changed locking scheme in ByteBuddyMockFactory from a
single global lock CACHE to cacheLocks with size 16.
The used TypeCachingLock from cacheLocks depends on
the hashcode of TypeCache.SimpleKey, which aggregates the types to mock.

The old global CACHE lock did block Threads regardless,
if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095

Co-authored-by: Björn Kautler <Bjoern@Kautler.net>
Vampire pushed a commit to spockframework/spock that referenced this pull request Sep 10, 2023
…1778)

Changed locking scheme in ByteBuddyMockFactory from a single global lock CACHE to cacheLocks with size 16. 
The used TypeCachingLock from cacheLocks depends on the hashcode of TypeCache.SimpleKey, 
which aggregates the types to mock.

The old global CACHE lock did block Threads regardless, if they try to mock the same type or not.

This is a similar fix as in Mockito:
mockito/mockito#3095
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.

Excessive locking in TypeCachingBytecodeGenerator#BOOTSTRAP_LOCK
4 participants