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 loading of same InterfaceImpl #5023

Open
MartinNowak opened this issue Jan 31, 2018 · 14 comments
Open

excessive loading of same InterfaceImpl #5023

MartinNowak opened this issue Jan 31, 2018 · 14 comments

Comments

@MartinNowak
Copy link

MartinNowak commented Jan 31, 2018

Environment

Provide at least:

  • jruby 9.1.15.0 (2.3.3) 2017-12-07 929fde8 Java HotSpot(TM) 64-Bit Server VM 25.144-b01 on 1.8.0_144-b01 +jit [linux-x86_64]
    jruby -J-Xms1g -J-Xmx1g -J-XX:+UseG1GC -J-XX:MetaspaceSize=192M -Xcompile.invokedynamic=true
  • Linux 4.4.0-109-generic Corrected a few typos #132-Ubuntu SMP Tue Jan 9 19:52:39 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Other relevant info you may wish to add:

  • jbundler, pry, and rake
  • vert.x-3.5.0, cassandra-driver, kafka, and rxjava (all java libs)

Expected Behavior

  • Identical interface implementations should be cached.

Attached a verbose class loading log, digged into this when seeing 2+M loaded classes in JConsole, most being collected again, ~50K classes remain resident.

Jan 31 11:49:32 bundle[18113]: [Loaded org.jruby.gen.InterfaceImpl702540685 from __JVM_DefineClass__]
Jan 31 11:49:32 bundle[18113]: [Loaded org.jruby.gen.InterfaceImpl702540685 from __JVM_DefineClass__]
Jan 31 11:49:32 bundle[18113]: [Loaded org.jruby.gen.InterfaceImpl702540685 from __JVM_DefineClass__]

iface_impl.log

With -Xinterfaces.useProxy=true I'm only seeing ~13K resident classes and ~750MiB less memory being used (guess that's Metaspace), so that seems like a viable workaround for now.

@kares
Copy link
Member

kares commented Jan 31, 2018

believe they're cached to some extend.
what would help to confirm a bug is a reproducer, do you know where they're created from?
would you care to attempt a minimal reproducer - where you expect the same Impl class to be reused and can confirm it isn't.

@enebo
Copy link
Member

enebo commented Jan 31, 2018

I half wondered if vert.x isolates via classpaths and perhaps confuses a cache as needing to regenerate something?

@MartinNowak
Copy link
Author

believe they're cached to some extend.
what would help to confirm a bug is a reproducer, do you know where they're created from?
would you care to attempt a minimal reproducer - where you expect the same Impl class to be reused and can confirm it isn't.

If I can find time, I'll try modify jruby's source to print the interfaces, that should help me to figure out which instance is causing this. Right now I can hardly locate it by any means, or would you have a another idea?
Cannot promise anything as reducing it sounds like at least half a day of work.

On the upside the -Xinterfaces.useProxy=true reduced CPU load for a Kafka consumer from 120% to 10%, so I'm glad I only had this single issue.

@MartinNowak
Copy link
Author

I half wondered if vert.x isolates via classpaths and perhaps confuses a cache as needing to regenerate something?

Could you expand a bit on that, I don't understand what you mean.

@enebo
Copy link
Member

enebo commented Feb 1, 2018

Ignore that comment. I was wondering how we generated real classes relative to classloaders. in order to load code all generated types must be reachable from a classloader. If there was some scenario where an execution environment stood up new CLs then the generated types would never be in the right CL and we would generate the thing again (old one eventually GCing). I cannot concretely explain that so it was just an off the cuff comment.

@MartinNowak
Copy link
Author

MartinNowak commented Feb 3, 2018

Here is a reproducer, actually seems to be more of a diagnostic issue, though I still wonder if it could be cached.
You can find guava-20.0.jar here.

require './guava-20.0.jar'
# https://github.com/google/guava/wiki/ListenableFutureExplained
# Guava ListenableFuture used by asnyc cassandra API
module Guava
  java_import 'com.google.common.util.concurrent.Futures'
  java_import 'com.google.common.util.concurrent.FutureCallback'
  java_import 'com.google.common.util.concurrent.SettableFuture'
end

$sum = 0

class FutureCallback < Guava::FutureCallback.class
  def onSuccess(v)
    $sum += v
  end

  def onFailure(e)
    puts e
    exit(1)
  end
end

class Dummy
end

1000.times do |i|
  fut = Guava::SettableFuture.create
  Guava::Futures.addCallback(
    fut,
    # argument dynamically adapted to the Guava::FutureCallback interface on every call
    FutureCallback.new(),
    ->(action) { action.run }
  )
  fut.set(i)
end

ok = $sum == 1000 * 999 / 2
puts ok ? 'OK' : 'FAIL'
exit(ok ? 0 : 1)

So the problem is that the java method accepts any ruby class as interface argument and only binds the method at call time. This is prolly done to support things like MethodMissing, but it's easy to run into and hard to diagnose.

The reason why it's

class FutureCallback < Guava::FutureCallback.class

is because I misunderstood the error message for

class FutureCallback < Guava::FutureCallback

TypeError: superclass must be a Class (#<Class:Java::ComGoogleCommonUtilConcurrent::FutureCallback> given).

Searching the net hinted towards

class FutureCallback
  java_implements Guava::FutureCallback
end

, but that is intended for generating a java class, not a ruby class.

The only correct way to implement a java interface in a ruby class is

class FutureCallback
  include Guava::FutureCallback
end

.
Of course I read https://github.com/jruby/jruby/wiki/CallingJavaFromJRuby#implementing-java-interfaces-in-jruby in advance, but back then that failed, apparently for other reasons.

Two actions that would improve the status quo.

  • Better error message when deriving from an interface.
TypeError: superclass must be a Class not a Java Interface (#<Interface:Java::ComGoogleCommonUtilConcurrent::FutureCallback> given)

instead of the

TypeError: superclass must be a Class (#<Class:Java::ComGoogleCommonUtilConcurrent::FutureCallback> given)
  • java_implements could check for unimplemented (java) interface methods (at least by name)

@kares
Copy link
Member

kares commented Feb 3, 2018

there's too many info for me, yet not sure about the reproducer, you expect the 'action.run' impl to generate a cached Java class, right?

@MartinNowak
Copy link
Author

there's too many info for me, yet not sure about the reproducer, you expect the 'action.run' impl to generate a cached Java class, right?

Not at all, it's about the FutureCallback ruby class being dynamically (ad-hoc) adapted to the Guava::FutureCallback interface on every single method invokation. That seems to be an intentional feature of JRuby, kind of duck-typing for interfaces.
The problem is that the ad-hoc interface adapter it's not cached and very easy to run into (no warning, still works), even though I tried to explicitly specify the interface that the Ruby class implements.

@MartinNowak
Copy link
Author

Hope that helps you as TL;DR @kares

@headius
Copy link
Member

headius commented Feb 13, 2018

If you want to implement FutureCallback, just do

class Blah
  include Guava::FutureCallback
...
end

The implementation is generated at (I believe) construction time and not regenerated for future objects. The pattern you use in your reproduction may be regenerating the impl class because it's doing it at the point where you pass the object, rather than in the object's natural class.

This indicates a problem with the overhead of dynamically implementing interfaces in an arbitrary object. Doing it a bit more statically by including the interface into a concrete class should work around it.

@MartinNowak Please try the code above for your FutureCallback impl and let us know how it looks.

@MartinNowak
Copy link
Author

MartinNowak commented Feb 21, 2018

If you want to implement FutureCallback, just do

class Blah
include Guava::FutureCallback
...
end

The implementation is generated at (I believe) construction time and not regenerated for future objects. The pattern you use in your reproduction may be regenerating the impl class because it's doing it at the point where you pass the object, rather than in the object's natural class.

Yes, that's what I figured out after a lot of debugging (see #5023 (comment)).
The error message for the intuitive interface implementation syntax was misleading and misled to sth. that was silently ignored but still succeeded (silently) due to dynamic interface implementation (duck typing).
A fixed error message for class MyRuby < JavaInterface would have been very helpful to avoid the problem, and maybe a switch to disable the dynamic interface implementation altogether.

@headius
Copy link
Member

headius commented Feb 28, 2018

I would personally be fine with disabling the feature altogether. I think this is the first time I've seen it used in the wild. I'm not sure I realized it even existed!

However, I think implemented properly it could still be efficient and not leak references as in #4968.

Ideally, each interface should only ever need a single class generated for these cases, since they all do the same thing: dynamically dispatch all methods from the interface.

Anyone else have an opinion on whether to keep and fix the feature or just remove it?

@headius
Copy link
Member

headius commented Apr 6, 2021

@kares @byteit101 Ok we need to revisit this. Are we doing a better job of caching the temporary interface impls generated when an object dynamically implements an interface when calling Java? We should also consider deprecating or removing the feature that allows you to extend an interface with the class MyClass < JavaInterface syntax.

@donv
Copy link
Member

donv commented Nov 22, 2023

Just encountered this in production. The fix for us was to implement the required Java interface using include:

class MyResponse
  include javax.servlet.http.HttpServletResponse
  ...
end

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

No branches or pull requests

5 participants