Skip to content

Conversation

@stevenschlansker
Copy link
Contributor

This should improve performance due to the new design of method handles.
Core reflection has to evaluate everything at the time that you call method.invoke - it has to do access checks, determine what parameter conversions are necessary, etc - and does not have any local state to save the result of that work.

With MethodHandle, you do more of the work up-front - determine access, determine what parameter boxing or casts are necessary - and store that computed state in a new MethodHandle. Later, when you invokeExact, the method handle is able to make the call much faster.

@cowtowncoder
Copy link
Member

Also looks like Android compatibility is broken? (wrt animal-sniffer failure for JDK 17 build)

@pjfanning
Copy link
Member

Java 17 compile issues

Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:614: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:6[9](https://github.com/FasterXML/jackson-databind/actions/runs/14025726660/job/39263945347?pr=5046#step:6:10)3: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java:814: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/util/ClassUtil.java:859: Undefined reference: boolean java.lang.reflect.AccessibleObject.trySetAccessible()
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:161: Undefined reference: void java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:190: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:208: Undefined reference: void java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/deser/impl/MethodProperty.java:225: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object, Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:[12](https://github.com/FasterXML/jackson-databind/actions/runs/14025726660/job/39263945347?pr=5046#step:6:13)7: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact()
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:148: Undefined reference: Object java.lang.invoke.MethodHandle.invokeExact(Object)
Error:  /home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/introspect/AnnotatedMethod.java:160: Undefined reference: Object java.lang.invoke.MethodHandle.invoke(Object)
Error:  Failed to execute goal org.codehaus.mojo:animal-sniffer-maven-plugin:1.24:check (default-cli) on project jackson-databind: Signature errors found. Verify them and ignore them with the proper annotation if needed. -> [Help 1]
Error:  
Error:  To see the full stack trace of the errors, re-run Maven with the -e switch.
Error:  Re-run Maven using the -X switch to enable full debug logging.
Error:  
Error:  For more information about the errors and possible solutions, please read the following articles:
Error:  [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
Error: Process completed with exit code 1.

@iProdigy
Copy link
Contributor

Java 17 compile issues

I believe those animal sniffer warnings are erroneous & can be suppressed: mojohaus/animal-sniffer#67

also the min android sdk for MethodHandles is 26, which is already applied

@pjfanning
Copy link
Member

Java 17 compile issues

I believe those animal sniffer warnings are erroneous & can be suppressed: mojohaus/animal-sniffer#67

also the min android sdk for MethodHandles is 26, which is already applied

Can't we increase the min Android SDK?

@iProdigy
Copy link
Contributor

iProdigy commented Mar 24, 2025

Can't we increase the min Android SDK?

To clarify, jackson 3 declares compatibility with sdk 34+ while 2.14 - 2.18 declares sdk 26+, both of which support MethodHandles - I don't follow your question

@pjfanning
Copy link
Member

I'd prefer to find out from animal-sniffer team why these methods are causing issues for it instead of just ignoring the issues that animal sniffer seems to think there are.

@cowtowncoder
Copy link
Member

Maybe there are newer versions of Animal sniffer configs (dep version from pom.xml)?

@yawkat
Copy link
Member

yawkat commented Mar 24, 2025

A new sniffer config is not sufficient, as PolymorphicSignature methods can have an unlimited number of signatures. It needs real plugin support.

I also don't think we can expect a response from the maintainers anytime soon, judging by that issue and the linked issues. Netty even removed the sniffer plugin because of it: netty/netty#14032

I think the the only sensible path forward is to add an exclusion for MethodHandle for now

@cowtowncoder
Copy link
Member

cowtowncoder commented Mar 24, 2025

@yawkat Ok. I will update dependency just as routine maintenance (for 2.19 and master, both behind a bit), but not expecting this particular issue to be addressed.
It does sound like an override needed then.

See #5047.

@stevenschlansker
Copy link
Contributor Author

I added an ignore for MethodHandle.
Now, the remaining AnimalSniffer problem is:

/home/runner/work/jackson-databind/jackson-databind/src/main/java/tools/jackson/databind/util/ClassUtil.java:859: Undefined reference: boolean java.lang.reflect.AccessibleObject.trySetAccessible()

I checked the definition and it seems that the method is part of the java spec since Java 9. Surely Android is past Java 9 baseline at this point - should I add another ignore for this one too?

@iProdigy
Copy link
Contributor

Surely Android is past Java 9 baseline at this point - should I add another ignore for this one too?

android doesn't seem to implement that method on any SDK: https://developer.android.com/reference/java/lang/reflect/AccessibleObject

you'll have to switch to setAccessible (and catch the exception)

@stevenschlansker
Copy link
Contributor Author

Rats, that sucks. I'll make that update...

@stevenschlansker
Copy link
Contributor Author

Ok, I reworked the ClassUtil.checkAndFixAccess method. There is a behavior change - previously, if we could not setAccessible on a member, we would end up throwing an exception. Now, we just skip the property, since I kept running into things like random attempts to crack open e.g. sun.util.calendar.ZoneInfo which fails. LMK if this is a problem and we can discuss more.

@stevenschlansker
Copy link
Contributor Author

WTF, Android does not implement InaccessibleObjectException either?!

@cowtowncoder
Copy link
Member

Ok, I reworked the ClassUtil.checkAndFixAccess method. There is a behavior change - previously, if we could not setAccessible on a member, we would end up throwing an exception. Now, we just skip the property, since I kept running into things like random attempts to crack open e.g. sun.util.calendar.ZoneInfo which fails. LMK if this is a problem and we can discuss more.

Hmm. Ok, that might be ok. Was thinking that maybe "sun.*" was not checked as system class by ClassUtil.isJDKClass() (and if so, could just add), but it is included.

I think it'd be good to try to avoid these calls to be sure (instead of having to catch exception) but that might be yet bigger undertaking.

@stevenschlansker
Copy link
Contributor Author

I think it'd be good to try to avoid these calls to be sure (instead of having to catch exception) but that might be yet bigger undertaking.

Yeah, that's why I tried to replace it with trySetAccessible - while it is still overriding visibility rules, at least it fails normally instead of requiring an exception thrown / catch.

The good news is this only happens during inspection, not at deserialization time, so the performance impact would be muted.

cowtowncoder added a commit that referenced this pull request Mar 26, 2025
@cowtowncoder
Copy link
Member

cowtowncoder commented Mar 26, 2025

@stevenschlansker Excellent work! I am bit hesitant to merge this before 3.0.0-rc2, but could be merged right after -- esp. assuming we could get some performance numbers, maybe for https://github.com/FasterXML/jackson-benchmarks/ (I'll see if I could run some myself).

EDIT: from quick test runs, it looks like there's maybe +5% for regular JSON read via databind for this PR vs 3.0.0-rc1. Hoping to test out writes too.
So maybe somewhat limited benefit at least for tested case.

@yawkat
Copy link
Member

yawkat commented Mar 26, 2025

Maybe you can use a method handle to call trySetAccessible 😄

@stevenschlansker
Copy link
Contributor Author

Thanks @cowtowncoder I am excited to finally get this work in :)
I rebased and squished the MR. Please let me know if there's any other tasks necessary pre-merge.

I am bit hesitant to merge this before 3.0.0-rc2, but could be merged right after

That's fine by me. Were I making the call, I'd think that having the change in users' hands in an earlier RC for more testing would be better than delaying merging and finding problems closer to release - but again, 100% your call :)

EDIT: from quick test runs, it looks like there's maybe +5% for regular JSON read via databind for this PR vs 3.0.0-rc1.
Hoping to test out writes too.
So maybe somewhat limited benefit at least for tested case.

I'm not too surprised. There was a time, a long time ago, when reflection was quite slow. The JVM has come a long way and now reflective calls are much, much faster than they used to be. That said, 5% is 5%, so I will take it :) especially since it comes with little downside.

Maybe you can use a method handle to call trySetAccessible

@yawkat yes, that would work :) I think since this is called only during (de)serializer instantiation, though, it's not worth the effort for now.

@cowtowncoder
Copy link
Member

@stevenschlansker Yeah, I plan on having quite a few RCs out, so by "not next" just means 2-3 week delay. Sort of to isolate things. But I get the point of as-early-as-possible wrt weeding out bugs.

On performance, yeah, +5% is measurable and has some value.

But odd thing is this -- now testing serialization I see no difference whatsoever. That is... odd. Would suggest deserialization speed up would have something to do with setters (parameter) maybe?

Finally: one question on MethodHandles -- I noticed removal of some Field-based classes. I assume field access still exists; how is that now handled?
(I guess I should just spend time actually going through PR :) )

@stevenschlansker
Copy link
Contributor Author

Finally: one question on MethodHandles -- I noticed removal of some Field-based classes. I assume field access still exists; how is that now handled?

MethodHandles unify method calls and field get / set with unreflectGetter
and unreflectSetter which present field read / write as trivial getter and setter methods. So we just use that then treat everything as a method now.

@stevenschlansker
Copy link
Contributor Author

stevenschlansker commented Mar 27, 2025

But odd thing is this -- now testing serialization I see no difference whatsoever. That is... odd. Would suggest deserialization speed up would have something to do with setters (parameter) maybe?

It can be really hard to nail down performance on this kind of thing... my understanding from reading the literature is that it should help, but maybe hard to see among all the other work any particular workload does.

It's possible different shapes have different costs. For example when deserializing, method.invoke(arg) I think will always allocate an Object[] for varargs, whereas methodHandle.invokeExact(arg) should not need to do any varargs since it is a magic @PolymorphicSignature method

// 08-Sep-2016, tatu: wonder if we should verify it is `AnnotatedField` to be safe?
prop = new FieldProperty(propDef, type, typeDeser,
beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
if (!ClassUtil.checkAndFixAccess(mutator.getMember(), ctxt.getConfig().isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) {
Copy link
Member

Choose a reason for hiding this comment

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

I think ctxt.getConfig().isEnabled(...) can be shortened to ctxt.isEnabled(...) (there should be convenience accessor).

But aside from that, could you add a brief comment explaining reasons why code short-circuits here?
(cannot access I assume but... is that then quietly swallowing cases causing surprises?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a brief comment. I agree, some conditions regarding module visibility could quietly swallow members here, if we cannot call setAccessible on them. I think in those cases though the whole operation will fail anyway until you allow Jackson module to open your module with beans in it.

There might be some follow-up tweaking to this logic necessary. I got all the existing tests passing but I am sure there are new exciting corner cases to discover with module visibility.

Come to think of it, can we remove the MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS now? I believe with MethodHandles used, there should no longer be any performance benefit to calling setAccessible unconditionally. I am happy to file a follow-up PR if this is acceptable. If not, we could consider turning it off by default now.

Copy link
Member

@cowtowncoder cowtowncoder Apr 7, 2025

Choose a reason for hiding this comment

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

Good idea -- I'll file an issue for changing defaults, will merge this PR and we'll go from there.

Actual removal of that feature is an option too, but at least start by changing its
default.

EDIT: issue -> #5074

}

constructors.removeIf(constructor ->
constructor == null || (_collectAnnotations && _intr.hasIgnoreMarker(_config, constructor)));
Copy link
Member

@cowtowncoder cowtowncoder Apr 4, 2025

Choose a reason for hiding this comment

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

Why is null check needed? It wasn't there earlier?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's now necessary because access checks have been pushed up from the point of use (when constructor is called) to earlier in deserializer creation (when constructors and factories are collected) so they are now filtered out earlier too. I added a comment.

Copy link
Member

@cowtowncoder cowtowncoder left a comment

Choose a reason for hiding this comment

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

Looks pretty good, only minor final touches and I can merge it.

@cowtowncoder cowtowncoder merged commit b47a371 into FasterXML:master Apr 7, 2025
6 checks passed
@cowtowncoder
Copy link
Member

@stevenschlansker Ok, looks like this also broke most downstream modules... :-(
Cascading builds at least gives that info in ~15 minutes after merge.

Will file issues, add a notes/refs here.

@cowtowncoder
Copy link
Member

cowtowncoder commented Apr 7, 2025

@cowtowncoder
Copy link
Member

Ugh. Seems like almost everything downstream fails, more or less catastrophically.

Will likely need to revert this PR later tonight.

@stevenschlansker
Copy link
Contributor Author

😢

@stevenschlansker
Copy link
Contributor Author

I'll try to find some time in the next week or two to check out the downstream modules and get those working.
I'm sure it's not trivial to set up, but it could be helpful to have a CI check that all of Jackson builds together.

@cowtowncoder
Copy link
Member

I'll try to find some time in the next week or two to check out the downstream modules and get those working. I'm sure it's not trivial to set up, but it could be helpful to have a CI check that all of Jackson builds together.

I have been trying to think of that over time and haven't thought of good maintainable solution.
Cascading rebuild is the first and only improvement -- which at least tightens up feedback loop from days (a week or more, even) to less than an hour.

Resource-wise full rebuild job for all PRs just for databind might be doable, although not sure how heavy resource-wise it'd be wrt Github charging (FasterXML has paid, not free, account).

@cowtowncoder
Copy link
Member

Will revert now.

@cowtowncoder
Copy link
Member

Reverted for now. Although failure was wide, it is possible there might be just small number of actual issues so maybe focusing on solving problems by just one downstream repo (like jackson-dataformat-xml) would make sense.

// 08-Sep-2016, tatu: wonder if we should verify it is `AnnotatedField` to be safe?
prop = new FieldProperty(propDef, type, typeDeser,
beanDesc.getClassAnnotations(), (AnnotatedField) mutator);
// 06-04-2025, scs: we cannot always see members if e.g. they are in a different module that does not
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to take a performance hit here? Why not let this fail if access is not setup? Null seems like a hack to me. The Java exceptions are fairly informative about the right add-opens are needed. @cowtowncoder wdyt? This looks like something we could do without. I only have a mobile to check this so the small screen view might not be the right way to check this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure what performance hit you are thinking -- this is not perf sensitive path (it's during deserializer construction).

I agree it seems odd to return null but I don't remember reasoning over straight failure.
@stevenschlansker is author here.
One thing to note tho is that ClassUtil.checkAndFixAccess just blindly overrides access if I recall and does not attempt to determine if call would likely fail -- it is just based on configuration settings. That could probably be improved, but we cannot test call here (we are not deserializing, we are building deserializer) to make sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From memory, the reason for this is because the access checks moved from "Method invoke time" to "MethodHandle create time" - this is one of the potential benefits of MH, lifting access / visibility checks from each-invocation to once-at-creation.

Some JDK classes in the modular world are not visible to user code - you're not allowed to crack open java.lang anymore, for example, without various --add-opens directives.

Jackson creates deserializers for types that are not used in some cases (I don't recall which) but basically this is skipping a failure in the case where we see some JDK type that we cannot crack open (Date IIRC?), but it does not matter, because the deserializer does not get used. So we skip an irrelevant failure.

I agree null is a bit weird. Improvements welcome, but the code is that way for a reason at least :)

Copy link
Member

Choose a reason for hiding this comment

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

Ok. @pjfanning if you wanted to try a PR with explicit fail, we could see if any of unit tests fail? (or just test locally).

@stevenschlansker Yeah I do not doubt there was (is) a reason (I vaguely recall discussions). But also do not remember context well enough to know the "right way" to go.

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.

5 participants