Hey all, I' trying to compile clojure-lsp
(#lsp) with Graalvm, with the help of @borkdude we managed to make it work with sqlite ๐ ,but I'm still having other issues, this time related to log4j
originally we hoped for interop with other java libraries logging that we use beneath but that never materialized since we need structured logs and not some freetext mayhem
@kulminaator @ericdallo Havenโt tried with GraalVM native image, but it doesnโt use any reflection or dynamic namespace loading https://cambium-clojure.github.io/ except discovering the current *ns*
, which clojure.tools.logging
also does.
A simpler alternative could be java.util.logging
that is built into Java. The clojure.tools.logging
wrapper loads classes at runtime https://github.com/clojure/tools.logging/blob/master/src/main/clojure/clojure/tools/logging/impl.clj#L52
or don't use java logging at all and use timbre for example?
Thereโs a page on logging in native image: https://www.graalvm.org/reference-manual/native-image/Logging/
Thanks for the help @kulminaator @kumarshantanu @borkdude I think replacing the log with another lib it'd be faster indeed, we just need a lib that logs to a custom file and that's it. I'll take a look at timbre, it seems to nave no issues with graalvm Otherwise test https://cambium-clojure.github.io/
@borkdude I made a simple clojure sample using timbre to log to a file (using spit-appender
) ang got https://pastebin.com/g9wqxqbv when compiling with graalvm
Debugging timbre's code it seems to just use http://clojure.java.io to log https://github.com/ptaoussanis/timbre/blob/56d67dd274d7d11ab31624a70b4b5ae194c03acd/src/taoensso/timbre/appenders/core.cljc#L75-L105
@ericdallo > Error: Frame states being merged are incompatible: unbalanced monitors - locked objects do not match This seems to indicate you are again not using 1.10.2-rc3
or hmm timbre is using monitor-enter and monitor-exit by itself? hmmmm
yes, they are using it!
this is the same problem that Clojure had. You should probably make a PR to timbre to use clojure's locking
and not use monitor-enter/exit manually
i'm using latest clojure 1.10.2-rc3
oh, nice
I don't know what should I change, could you provide some materia/links?
I think I should follow this example: https://clojuredocs.org/clojure.core/locking
see https://github.com/clojure/clojure/commit/f5403e9c666f3281fdb880cb2c21303c273eed2d#diff-313df99869bda118262a387b322021d2cd9bea959fad3890cb78c6c37b448299
this is a very technical low level change, to ensure byte code analyzers can verify that the monitor enter and exit match up.
your PR would replace monitor enter/exit to just use clojure's locking
and not the current try catch
I see, I'll give a try, thanks @borkdude!
you can refer to CLJ-1472 for details
@ericdallo timbre allows using your own appender, so for now you could copy this code, fix it and use your custom appender
an upstream PR is of course nicer long term
Oh, good idea, I'm coding the fix for timbre right now, but I'll proceed with a custom adapter until the PR get merged
if that code is still reachable native-image might still complain, but you can patch with alter-var-root for the moment being
(fn self [data]
(let [{:keys [output_]} data]
(let [output (force output_)]
(locking (Object.) ; For thread safety, Ref. #251
(try
(with-open [^java.io.BufferedWriter w (jio/writer fname :append append?)]
(.write w ^String output)
(.newLine w))
(catch java.io.IOException e
(if (:spit-appender/retry? data)
(throw e) ; Unexpected error
(do
(jio/make-parents fname)
(self (assoc data :spit-appender/retry? true))))))))))
Does that makes sense?I removed the monitor usages replaced by the locking defmacro
should work
@borkdude would this code be enough to fix the issue right now?
https://pastebin.com/y1L3HEPU
WIth that I'm still having the same unbalanced monitors - locked objects do not match
This is the https://pastebin.com/gTB7QtB6.
Not sure the alter-var-root
is having effect
The thing with direct linking is, is that this default appender may be used in multiple places, so alter-var-rooting it only at the source spot won't suffice if that's the case
what about using a custom appender?
didn't work too, but I released a local version of my change on timbre and got other issue now, will paste the output
Oh, actually it worked after adding --allow-incomplete-classpath
noice, another issue solved ๐
@borkdude I think this is the only missing issue: https://pastebin.com/JPv6vZ0s
About the clojure.lang.Agent
It seems related to the threads that clojure-lsp spawns during the initialize
yeah, you should not do that on the top level
but in the -main
Hum, but we start that on main already: https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/main.clj#L427
you don't launch any agents at the top level?
can you pinpoint who is starting this thread?
Yep, that's what I'm trying to understanding why it's not working, we just call this function run
from the -main
https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/main.clj#L431
@ericdallo if you avoid the run
call, do you still get the error?
does not work even commenting the run @borkdude ๐ฎ
I'll keep commenting server declaration until the erros is gone, then we ll know what line is the offending one
maybe some other lib is spawning a future at the top level
try eliminating some libs from the ns decl
I've recently encountered this with some lib, don't remember which one it was
Yeah, probably some lsp4j class
you can try --initialize-at-runtime=<the name of the class>
Ok, found the offender require:
[nrepl.server :as nrepl.server]
weird, since we start the nrepl server here, but requiring it's enough to graal compile blow up: https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/main.clj#L414
which library is this?
nrepl/nrepl {:mvn/version "0.8.3"}
could be one of these... https://github.com/nrepl/nrepl/blob/master/src/clojure/nrepl/server.clj#L5-L15
why do you have an nREPL server in LSP?
Just searched graalvm and found a issue where you are mentioned ๐ https://github.com/nrepl/nrepl/issues/181
it sounds like this could be moved to a dev dependency
btw, I have an nREPL server in babashka which works with graalvm :P
built from the ground up
> it sounds like this could be moved to a dev dependency I think so
oh yes, now I remember!
Does babashka just require the same nrepl.server
? how does that work hahaha?
this completion namespace has top-level futures, very bad
I made a fork of this stuff to make reply (the lein REPL) work with graalvm
and I turned the futures into delays
๐ฎ
do you have the code, it'd be helpful to fix on nrepl too, right?
I just added you to the repo
I haven't open sourced the code since it's quite hacky
and copy/pasted the entire reply lib to hack on it
you have to many repos ๐ that's crazy and awesome at same time ๐
@ericdallo what you can maybe do is copy paste the nrepl completions namespace into your own repo
and then change the futures into delays
(or move nrepl to a dev dev... or some feature toggle)
moving to dev deps maybe is enough and the recommended IMO, I'll try to move to another profile so
this will also yield a smaller native image
Yes!
I just noticed that now it compiles but it seems that the app starts and then finish, while when running via lein run
it keeps the socket open waiting for the LSP IO
@borkdude it compiles clojure-lsp perfect, is just that it just exit the app, do I need to make anything more?
I pushed what I done so far here: https://github.com/clojure-lsp/clojure-lsp/pull/267
Oh, actually, it's throwing an exception at runtime and the log is going to the file, I didn't notice it ๐
2021-01-24T20:44:40.990Z gregnix-note INFO [clojure-lsp.main:415] - Starting server...
2021-01-24T20:44:41.000Z gregnix-note ERROR [taoensso.timbre:796] - Uncaught exception on thread: main
clojure_lsp.main.main
...
clojure-lsp.main/-main main.clj: 436
clojure-lsp.main/-main main.clj: 438
clojure-lsp.main/run main.clj: 418
org.eclipse.lsp4j.launch.LSPLauncher.createServerLauncher LSPLauncher.java: 46
org.eclipse.lsp4j.jsonrpc.Launcher$Builder.create Launcher.java: 321
org.eclipse.lsp4j.jsonrpc.Launcher$Builder.createProxy Launcher.java: 364
org.eclipse.lsp4j.jsonrpc.services.ServiceEndpoints.toServiceObject ServiceEndpoints.java: 41
...
com.oracle.svm.reflect.proxy.DynamicProxySupport.getProxyClass DynamicProxySupport.java: 113
com.oracle.svm.core.util.VMError.unsupportedFeature VMError.java: 86
com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.eclipse.lsp4j.services.LanguageClient, interface org.eclipse.lsp4j.jsonrpc.Endpoint] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
Ok, after adding H:DynamicProxyConfigurationResources
with those classes it seems to work ๐ , let me test it
cool
I still could not make it work ๐ It compiles and start without any issues, but it seems to be some wrong classes from the same interface... It's using the https://github.com/eclipse/lsp4j/blob/master/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/GenericEndpoint.java#L38 instead of the https://github.com/eclipse/lsp4j/blob/master/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/RemoteEndpoint.java#L46, so when LSP client (lsp-mode) calls the server first methods (initialize), it returns this error:
Jan 24, 2021 9:28:05 PM org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint request
WARNING: Unsupported request method: initialize
This is something that seems relevant, the dynamic_proxy.json:
[
["org.eclipse.lsp4j.services.LanguageClient", "org.eclipse.lsp4j.jsonrpc.Endpoint"]
]
BTW https://github.com/eclipse/lsp4j/blob/master/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/services/ServiceEndpoints.java#L38-L55 that creates the proxy dynamically that I needed to setup this โ๏ธ
I already tried to change https://github.com/clojure-lsp/clojure-lsp/pull/267/files#diff-47adefe2f307a723b07cd0862a4478ec5d038fa6a8ba1733f92560c21e2f49bbR299 to reify
instead of proxy
also.
I feel like this is the last error or something like that ๐คI'm stuck with this last error โ๏ธ found this on lsp4j side: https://github.com/eclipse/lsp4j/issues/349 This is the only thing I get it on strerr:
Jan 26, 2021 8:06:04 PM org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint request
WARNING: Unsupported request method: initialize
While other logs not using graalvm say something like that:
Jan 26, 2021 7:02:07 PM org.eclipse.lsp4j.jsonrpc.RemoteEndpoint handleCancellation
WARNING: Unmatched cancel notification for request id 9710
what makes me guess that for some reason, it's using the wrong implementation of the lsp4j Endpoint interfaceI compiled with graalvm 21 to check if there anything, but still the same
cool. I have no clue and I'm going to bed soon. keep me posted
I kind of found the issue, I made a simple reify of the LanguageServer
from lsp4j
:
(defn fake-server []
(reify LanguageServer
(^CompletableFuture initialize [_this ^InitializeParams params]
(do
(println "INITIALIZING!")
(log/info "INITIALIZING!")))
(getTextDocumentService [_this]
nil)
(getWorkspaceService [_this]
nil)))
During the start of the app, I call:
(log/info "-->" (class server)
(:members (r/reflect server)))
That prints all methods from the class, it works perfect with lein run
,but with graalvm it prints #{}
So what does this tell you?
It means the for some reason, graalvm is not working with that reify
That is just a simple interface: https://github.com/eclipse/lsp4j/blob/master/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/services/LanguageServer.java#L28
@ericdallo what error do you get when using it?
Jan 27, 2021 9:46:32 AM org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint request
WARNING: Unsupported request method: initialize
because it can't find the implementation of one of the methods initialize
because you haven't reified it?
oh sorry I misread
this is what I get from lein run
/ java
from graalvm home:
--> clojure_lsp.main$fake_server$reify__31521 #{#clojure.reflect.Method{:name initialize, :return-type java.util.concurrent.CompletableFuture, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [org.eclipse.lsp4j.InitializeParams], :exception-types [], :flags #{:public}} #clojure.reflect.Method{:name getTextDocumentService, :return-type org.eclipse.lsp4j.services.TextDocumentService, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [], :exception-types [], :flags #{:public}} #clojure.reflect.Field{:name const__4, :type java.lang.Object, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Method{:name meta, :return-type clojure.lang.IPersistentMap, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [], :exception-types [], :flags #{:public}} #clojure.reflect.Field{:name const__2, :type clojure.lang.Var, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Method{:name getWorkspaceService, :return-type org.eclipse.lsp4j.services.WorkspaceService, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [], :exception-types [], :flags #{:public}} #clojure.reflect.Field{:name const__1, :type clojure.lang.Var, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Field{:name const__6, :type clojure.lang.Keyword, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Method{:name withMeta, :return-type clojure.lang.IObj, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [clojure.lang.IPersistentMap], :exception-types [], :flags #{:public}} #clojure.reflect.Constructor{:name clojure_lsp.main$fake_server$reify__31521, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [clojure.lang.IPersistentMap], :exception-types [], :flags #{:public}} #clojure.reflect.Constructor{:name clojure_lsp.main$fake_server$reify__31521, :declaring-class clojure_lsp.main$fake_server$reify__31521, :parameter-types [], :exception-types [], :flags #{:public}} #clojure.reflect.Field{:name const__3, :type clojure.lang.Keyword, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Field{:name const__0, :type clojure.lang.Var, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Field{:name const__5, :type clojure.lang.Keyword, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}} #clojure.reflect.Field{:name __meta, :type clojure.lang.IPersistentMap, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:final}} #clojure.reflect.Field{:name const__7, :type java.lang.Object, :declaring-class clojure_lsp.main$fake_server$reify__31521, :flags #{:public :static :final}}}
It's printing correct all methods from that reify@ericdallo Maybe this will work: Is it possible to make the reified object at the top-level, at compile time?
yes it's possible, changing from defn
to def
would be enough?
think so
Let me try it :loading:
Same issue, this is the output of that print from above:
--> class clojure_lsp.main$reify__31521 #{}
also the same error when used?
yep, maybe try with gen-class? https://stackoverflow.com/a/8614783
or add the class to the reflection config
@ericdallo Maybe try once without type-hinting the initialize
method? Since itโs the only method in the interface, it should work without hinting due to auto-inference.
But how can I get the class name? or you mean add the interface to the reflection config?
good point @kumarshantanu, I'll need type hint for other methods later, but for that test it's valid, let me try
yeah, the interface
It gives another error, but it seems is a step ahead ๐
SEVERE: Unable to invoke no-args constructor for class org.eclipse.lsp4j.adapters.InitializeParamsTypeAdapter$Factory. Registering an InstanceCreator with Gson for this type may fix this problem.
java.lang.RuntimeException: Unable to invoke no-args constructor for class org.eclipse.lsp4j.adapters.InitializeParamsTypeAdapter$Factory. Registering an InstanceCreator with Gson for this type may fix this problem.
at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:226)
at com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory.getTypeAdapter(JsonAdapterAnnotationTypeAdapterFactory.java:55)
at com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory.create(JsonAdapterAnnotationTypeAdapterFactory.java:49)
at com.google.gson.Gson.getAdapter(Gson.java:423)
at com.google.gson.Gson.fromJson(Gson.java:887)
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.fromJson(MessageTypeAdapter.java:329)
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.parseParams(MessageTypeAdapter.java:249)
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.read(MessageTypeAdapter.java:119)
at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.read(MessageTypeAdapter.java:55)
at com.google.gson.Gson.fromJson(Gson.java:888)
at org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler.parseMessage(MessageJsonHandler.java:119)
at org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler.parseMessage(MessageJsonHandler.java:114)
at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.handleMessage(StreamMessageProducer.java:193)
at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen(StreamMessageProducer.java:94)
at org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run(ConcurrentMessageProcessor.java:113)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.lang.Thread.run(Thread.java:834)
at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:519)
at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:192)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Method.java:566)
at com.google.gson.internal.UnsafeAllocator$1.newInstance(UnsafeAllocator.java:50)
at com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:223)
... 21 more
Caused by: java.lang.IllegalArgumentException: Class org.eclipse.lsp4j.adapters.InitializeParamsTypeAdapter$Factory is instantiated reflectively but was never registered. Register the class by using org.graalvm.nativeimage.hosted.RuntimeReflection
at com.oracle.svm.core.graal.snippets.SubstrateAllocationSnippets.hubErrorStub(SubstrateAllocationSnippets.java:247)
at sun.misc.Unsafe.allocateInstance(Unsafe.java:840)
... 24 more
Process clojure-lsp stderr<1> finished
Not sure is related to remove the type hinting or to move to the reflection.json
never change two things at a time ;)
yeah hahaha
it seems another issue now, it's serializing the json to respond to client
Probably I should do something to initialize Gson correctly
Oh this looks a good start:
Class org.eclipse.lsp4j.adapters.InitializeParamsTypeAdapter$Factory is instantiated reflectively but was never registered. Register the class by using
should I add it to reflection.json?
guess so
If that works, probably I'll need to add https://github.com/eclipse/lsp4j/tree/master/org.eclipse.lsp4j/src/main/xtend-gen/org/eclipse/lsp4j/adapters to the reflection too, if that works, I'll certainly open an issue on lsp4j with steps how to make it work with graalvm ๐ค
Good news, problem solved with the suggestion from you, @borkdude , I added some classes to reflection json and it's getting into the method initialize correctly now โ The next issue ๐ is: clojure-lsp tries to parse Gson json objects via https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/interop.clj#L416-L432, and it's returning this error on runtime with graal:
com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine PosixJavaThreads.java: 192
com.oracle.svm.core.thread.JavaThreads.threadStartRoutine JavaThreads.java: 519
java.lang.Thread.run Thread.java: 834
java.util.concurrent.ThreadPoolExecutor$Worker.run ThreadPoolExecutor.java: 628
java.util.concurrent.ThreadPoolExecutor.runWorker ThreadPoolExecutor.java: 1128
java.util.concurrent.FutureTask.run FutureTask.java: 264
java.util.concurrent.Executors$RunnableAdapter.call Executors.java: 515
org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run ConcurrentMessageProcessor.java: 113
org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen StreamMessageProducer.java: 94
org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.handleMessage StreamMessageProducer.java: 194
org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.consume RemoteEndpoint.java: 190
org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.handleRequest RemoteEndpoint.java: 261
org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.request GenericEndpoint.java: 120
org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.lambda$null$0 GenericEndpoint.java: 65
...
clojure_lsp.main.proxy$clojure_lsp.ClojureExtensions$LanguageServer$c8d5825a.initialize
clojure-lsp.main/fn/fn main.clj: 326
clojure-lsp.main/client-settings main.clj: 278
clojure-lsp.interop/json->clj interop.clj: 421
...
java.lang.IllegalArgumentException: No matching field found: isJsonNull for class com.google.gson.JsonObject
Should I add com.google.gson.JsonObject
to reflection too?
yes
at least that method
After that I get:
java.lang.NoSuchMethodError: java.lang.reflect.AccessibleObject.canAccess(java.lang.Object)
Not sure what to add to reflection.json nowMaybe java.lang.reflect.AccessibleObject
๐
Yeah, that was my guess ๐ compiling :loading:
you can also run with a graalvm agent and then it will spit out all the things it needed reflection for during the run of a program
it will also spit out a lot of classes you don't need, but it might give you some idea
be sure to run with direct linking, this might help
Yeah, I thought about that, but the issue is that is not trivial to test clojure-lsp on terminal, we need to send LSP spec jsons, I'll try to move it to a script that use the java from graalvm, hope it works
I mean, this is just a setting when creating the uberjar
-J-Dclojure.compiler.direct-linking=true
oh
I see, never mind
np, thanks for the tip ๐
Having this now:
Can't call public method of non-public class: public sun.nio.fs.UnixPath sun.nio.fs.UnixPath.getParent()
even with this on reflection.json:
{
"name": "sun.nio.fs.UnixPath",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
}
Related to this line: https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/crawler.clj#L199@ericdallo Try hinting dir
as a public interface? (.getParent ^java.nio.file.Path dir)
Hum, good guess again, let me try it, I was thinking in refactor that to use <http://clojure.java.io|clojure.java.io>
instead of manually using java classes, similar how clj-kondo do to get the config dir
@ericdallo You should never have to include OS specific things in your reflection config, this config won't work on Windows for example
I think you need a type hint here: https://github.com/clojure-lsp/clojure-lsp/blob/0b881960cbce9cb09a6bbb8738a94f390b205d94/src/clojure_lsp/crawler.clj#L196
either for the return type or for the local
You are right, something like that?
[dir ^java.nio.file.Path (shared/uri->path project-root)]
@borkdudeyes, but if you place this type hint on your function it would be even better
Ok, so the type hinting fixed that case, but It being really hard to make lsp4j work correctly, it looks it uses a lot of reflection and I'm not sure I'm on the right path... The https://github.com/clojure-lsp/clojure-lsp/pull/267/files#diff-736ec9c37d3ba754db0a1f0c6cc5df40e8f284fb11e4824e55920cce814c37d8R1 already looks huge and I started to think that adding some classes have caused inconsistency like request params objects come with some fields null and etc. Also I could not run the java from graalvm to check what should looks like my reflection.json since is not available for my NixOS ๐
@ericdallo If you add this to your project.clj
, you might get all the reflection warnings with lein
before even trying GraalVM:
:global-vars {*warn-on-reflection* true
*assert* true
*unchecked-math* :warn-on-boxed}
this won't help for java code which uses reflection
for this you can use the graalvm agent
Yep, that's the agent that should be used with graal's java that is not available for NixOs :/
you can just download graalvm to a Downloads directory. there is no need to "install". Or aren't you allowed to downloading anything in nixOS?
I can if it don't use any other lib
I will give a try so
yes, it's standalone. just unzip it and set GRAALVM_HOME
to the thing and then use $GRAALVM_HOME/native-image
this is how I use multiple graalvm versions side by side
also run $GRAALVM_HOME/gu install native-image
$GRAALVM_HOME/bin/gu install native-image
to be precisethanks!
but to run with graal's java it should be something like $GRAALVM_HOME/bin/java
I guess
that's right. you can export GRAALVM_HOME on the path like export PATH="$GRAALVM_HOME/bin:$PATH"
and then just run java
It doesn't work, it depends on libs from SO ๐
This is what I install on CircleCI:
sudo apt-get -y install gcc g++ zlib1g-dev
I'm sure there is a nixOS incantation for this?
you can always run this in a docker I guess
I tried the docker before, but failed for some reason, I gave a try again and it worked run the java from graal with the agent on NixOS via docker:tada:
I got a huge relfect json config generated with a lot of classes from clojure and some with dynamic names (using the object reference Id)
Yeah, there is a lot of stuff in there that you don't need. I'm not sure why it's showing up. Maybe it helps if you run with direct linking.
-J-Dclojure.compiler.direct-linking=true
I'm afk now
Ok, use the direct-linking helped reducing some weird classes, I used the output config to the native image input and the result looks better than before ๐
now it seems to load LSP correctly and communication work (json ser/deser), but I'm having a issue when responding with clojure.spec.alpha/conform
https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/interop.clj#L409.
Basically we use this function everytime we want to respond to client, so we validate if server response is following the spec and do some clj-map->java transformations (nothing magical, just instantiating the class manually)
https://pastebin.pl/view/6450ceea
It was related with some instances not being recognized in runtime, fixed it using some type hints ๐
Hey @borkdude Just got a corner case with sqlite ๐
It seems to throw a segfault when the db does not exists: (missing .lsp/sqlite.db
)
https://pastebin.com/JqHJPJzk
Right on this line: https://github.com/clojure-lsp/clojure-lsp/blob/master/src/clojure_lsp/db.clj#L35
@ericdallo I don't know what causes this. Where did you get the suggestions for the reflection / JNI config for sqlite?
Right here: https://github.com/xerial/sqlite-jdbc/issues/413
From the example: https://github.com/xerial/sqlite-jdbc/issues/413#issuecomment-525926912
@ericdallo Does his Java example work correctly when the db does not exist?
Hum... I didn't test, good point, I'll test it
And to double check, you added all these classes to the JNI config right? https://github.com/mageddo/graalvm-examples/blob/59f1f1bf09894681edfddaa100b4504770ad0685/sqlite/src/main/java/com/mageddo/sqlite/JNIReflectionClasses.java#L46-L55 and to reflection
Also try graalvm java 8 instead of 11
Yes, they are on https://github.com/ericdallo/sqlite-jni-graal-fix workaround
reflection not really
Oh, just found some classes missing from the fix, I don't know why, I'll add them too
That is done here, reflection: https://github.com/ericdallo/sqlite-jni-graal-fix/blob/61aba255490c5facbc94cabd43e22aff6a69e5a3/src-java/ericdallo/JNIReflectionClasses.java#L80
Oh, yeah, so it's enough probably
I realized it's missing the Throwable class
since when don't find the db it should throw an exception, may be related
yeah
Btw, Quarkus is a Java framework by Oracle of which all extensions can be compiled to native. Sometimes nice to borrow stuff from as well: https://github.com/quarkusio/quarkus/tree/master/extensions/jdbc
Hum, interesting
Yep, it fixed the issue ๐ Probably it was the missing Throwable... that is not on that repo master, only on that specific commit
cool
a segfault like that is really hard to debug...
yep
this is why you should probably not just switch to native, but offer this as something experimental ;)
and keep offering the JVM jar as well
among other reasons
but it's cool that you got sqlite to work. maybe I can switch my pod from go to clojure now ;) although the golang pod is really small (few megabytes)
does LSP native not require you to have sqlite installed, some libsqlite thing on your system? maybe try the binary in a bare ubuntu docker image (if that hasn't got it by default)?
Yeah, My idea is if everything work provides that as experimental for some time
Note that if you exclusively offer the native, you will probably have to provide it for the three major OSes and also a static one for alpine
yes, since I use NixOS there is no way to use a lib that I didn't tell it to use, but a test on a docker it'd really help to make sure
Yes, I have in mind setup a CI for the 3 OS building via GH actions, just like babashka
@borkdude how the nrepl just for dev would work? In the code should I check somehow what is the profile/some env var? This would affect graal?
I can add the dep for the dev profile only, but my question is how should I code that to use for dev only or something like that
yeah, you can do it through an env var or just by trying to resolve it on the top level:
(when-let [server-var (try (requiring-resolve 'nrepl.server/server) (catch Exception e nil))]
...
note that it's best to keep this at the top level so it's not resolved at run time
the native-image will compile this away
and then of course you move the nrepl deps to a dev profile
or alias
got it, do you use anything like that in your projects?
yeah, this is how all the features in babashka are implemented
see build.md, feature flags
cool, thanks!
@ericdallo I also have this library: https://github.com/borkdude/dynaload
Oh I liked this one: https://github.com/babashka/babashka/blob/6dc6cbab12910f6b93fa52c66e2f7da1a3213b09/src/babashka/impl/classes.clj#L322
that dynaload seems to fit perfect this use case
should I wrap the dynaload call with a try catch to check if nrepl is enabled? Or is better to check a env var?
What you can do is do a require based on an env var. And then use dynaload to use the var(s)
what is the point to use dynaload if I can just require 'mylist
? I think I don't get it
Oh, it won't compile if I don't require it.. it makes sense
dynaload just use a symbol and do the magic
this is more useful for libraries than apps probably. in libraries you can say: I can work with these vars, if you have required the lib already
in normal JVM dynaload will also do the require for you
but for GraalVM this has a negative impact on binary size
although it will still work
I see
To keep it simple: Just do a try catch where you try to require/resolve the lib/vars. If you then provide the library on your classpath, the vars will work. Else they won't do anything.
great
ok, nrepl fixed using dynaload ๐
I tried to use SLF4j, log4j, log4j2 and logback, but all have issues with file appenders with graalvm... so I think timbre will be the only way
I fixed all found issues with reflection, need to test all features still.
I noticed that the first clojure-lsp classpath scan time using clj-kondo was increased, for a proect with 250 files to scan, while with JVM it takes 90s with graalvm takes 130s, not sure there is anything to improve that though.
About the image size, I removed a lot of unnecessary reflection, still the image size is about 110MB while the jar has 30MB, I found https://github.com/ptaoussanis/timbre/issues/326 regarding timbre that adds 50MB to the image size but not sure is happening
What you suggest to decrease the image size @borkdude?
@ericdallo
> while with JVM it takes 90s with graalvm takes 130s, not sure there is anything to improve that though.
This is why I think the JVM makes most sense for projects like this, I keep recommending this for my own LSP server for clj-kondo
As for the image size, you can look at the analysis callgraph or use the dashboard options.
I see ๐ Besides that, everything is pretty much fast, like diagnostics, code actions, refacotings which improve user xp IMO I still need to make the memory test, then we can think in the tradeoff.
How can I check the analysis callgraph?
"-H:+PrintAnalysisCallTree"
this will dump some information about the pulled in classes, methods, etc
Nice, I'll try
and there is also a dashboard option: https://www.graalvm.org/docs/tools/dashboard/?ojr=help%3Btopic%3Dgetting-started.md
I haven't used the dashboard that much, but it seems useful. You can drill down into classes
@borkdude for some reason the dashboard flag just gives me OoO even with a -JXmx of 6GB :man-shrugging:
I did some hacking to run Graalvm 21.0.0 on My NixOS (docker doesn't work) instead of 20.2.0 and the startup time fall from 130s -> 75s... it's faster than JVM classpath scan ๐ฎ (~87-90s), I will make more tests, special the memomry one (not sure how to measure that yet)
Also, I added the new -H:+InlineBeforeAnalysis
flag, not sure this could cause that performance improvement though
@ericdallo Be sure to use the --no-fallback
option so you won't get a fallback image, this is effectively just a JVM
And yeah, the dashboard data may require moar memory
it's a lot of data
yeah, I'm using the --no-fallback, I don't want that kind of lie anymore ๐
You could also post an issue about a performance regression on the graalvm issue tracker
@borkdude sorry, I probably was not clear, I was using Graalvm 20.2 with all my tests because it's the only available for NixOS while this https://github.com/NixOS/nixpkgs/pull/105815 , also the docker image doesn't work with NixOS for some reason related with shared libs, but I managed to manually install the graalvm 21.0.0 in the nix way
so it's not a regression since I went from 20.2.0 -> 21.0 ๐
so 21 is faster right?
ok, then it's good!
yep, at least for the only thing I noticed that it was slower than JVM, now it's not anymore ๐
Using the dashboard, I can't see anything that is ocupying a huge space... it's ood that summing all those, it won't be equal to 110MB
A huge context here: https://clojurians.slack.com/archives/CPABC1H61/p1611087562030000 Hey @borkdude I think moving the discussion to here would be better for others with same problems
So, with https://github.com/clojure-lsp/clojure-lsp/pull/267/files it seems it has 2 issues right now at build-time, one regarding log4j and another about some clojure.lang.Agent class. both graalvm says that I should try use --initialize-at-run-time
, so after I added --initialize-at-run-time=org.apache.log4j.LogManager
, it indeed fixed the log4j error (but probably it'll happen at run-time when I got to work), but I have no idea how to fix the clojure.lang.Agent
error
Log: https://pastebin.com/cKXDiQPv
clojure-lsp uses log4j
to log to a custom file /tmp/clojure-lsp.out
, LMK If you think this can be replace with other lib that works with graalvm
started using log4j for a project 6 years ago, still regretting it but it's always waiting in the background to get the boot
my usecase is spilling out logs to file or stdout in json format so i can feed them to elastic .... and log4j is not helping me there in any way