-
-
Notifications
You must be signed in to change notification settings - Fork 53
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
fn-deps for lambdas #141
fn-deps for lambdas #141
Conversation
Small note - we can't really depend on |
ah thanks, I completely forgot I was still depending on that. I'll rewrite that part to use clojure/read. Do you have any thoughts on the idea of using the compiler to find the dependencies? |
The idea itself seems reasonable to me. |
I've got a new solution that doesn't need to recompile the code and is a lot faster. |
Can you explain this as a PR comment (self-review)? That way we can see the code that the problem in question relates to. |
src/orchard/xref.clj
Outdated
(when-let [^clojure.lang.AFn v (as-val s)] | ||
(let [f-class-name (-> v .getClass .getName) | ||
field (->> clojure.lang.DynamicClassLoader .getDeclaredFields second) | ||
classes (into {} (.get field clojure.lang.DynamicClassLoader)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this holds all functions compiled by clojure:
{...
"dev$dummy"
#object
[java.lang.ref.SoftReference 0x5081395f
"java.lang.ref.SoftReference@5081395f"]
...
}
where lambdas are named "dev$dummy$fn_1234"
the filter uses that fact to just keep the function + lambda classes.
The problem is, when you recompile a function, new lambda classes get generated and the old ones stay around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the old ones stay around
As in, garbage to be collected?
What causes that retention? This is a defn
(and not e.g. a def
), so why invoking a defn would retain memory references?
Unless there's global state somewhere, probably your solution is good - it's just minor details like *1
or other repl state that might retain objects for longer than expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably explained it wrong.
The problem is not the memory, the problem is that the Clojure classloader keeps the old classes in its own cache as java.lang.ref.SoftReference and only deletes that if you try to access the reference after it has been cleaned up.
So if I do the following in my code:
(defn dummy []
(map #(println %) (range 10)))
(defn dummy []
(map #(print %) (range 10)))
;;o r just send both versions to the repl by changing the first
classes will now contain "dev$dummy$fn_123" and "dev$dummy$fn_124" where 123 references println and 124 references print.
I haven't found an easy way to get rid of "dev$dummy$fn_123" yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it would be solved if WeakReference was used instead.
However, as mentioned personally I do not condone classloader hacking :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This solution does not overwrite the clojure classLoader, so I can't switch the reference type. This just accesses the regular clojure loader through reflection.
I probably shouldn't have kept two separate solutions in the same file, I can see how that is confusing, sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't found an easy way to get rid of "dev$dummy$fn_123" yet.
Does the PR still have this problem?
It might be acceptable to have that problem, if so at least leave a comment explaining what's going on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the PR still has the problem. I'll play around with it some more to see if I can find a solution. If I can't find one I'll add a comment describing the issue and open a new ticket once the PR is merged.
Part of the context is https://lukas-domagala.de/blog/clojure-analysis-and-introspection.html which says:
Personally I like the middle ground of using clj-kondo but spawn from the same JVM that hosts a given repl. That way the classpath is always up to date, and one doesn't need external binaries. clj-kondo itself uses a persistent cache so I don't believe saying it takes 40s to analyze a project is quite representative of day-to-day UX. As you, I much favor runtime-powered solutions where they make sense. Rich Hickey's hack is a hack, it might work for him because he authored the Clojure compiler. The compiler's internals are generally not for us to rely on. So clj-kondo is a fine solution, just as tools.analyzer is (which is very much runtime-powered, so survives any dynamism). My take would be that unless this PR becomes substantially more minimalistic, it would be not be worth the hassle. One can always use his own tooling on a personal stack - which I do all the time! Later one can propose PRs as those solutions become more time-proven. |
Thank you for your comments @vemv I've removed the recompile part of the code and commented on the weird IllegalAccess workaround. It's completely fine if you don't want to merge this. This way whoever stumbles over this will have a clean thing to build upon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR looks more minimalistic now so we can proceed, thanks!
Well, our built-in find-usages is not going away, so I'm in favor of improving it. I get @vemv's position but I think it's fine to keep improving what we have given that any alternative will require external deps and extra work that no one has been willing to do for years. :-) I also think that Clojure's compiler API has been super stable, so the concerns about whether it's a good idea to use its API are probably overdone. Let's drive this PR to the finish line! :-) |
I'm still trying to fix the "old Lamdba" reference problem but otherwise this is done. |
Kudos for the iteration! There's WIth WeakReference (vs. SoftReference), GCs are even more effective, but I realise we cannot fork Clojure. |
gc does not help, it only clears weakReferences. Softreferences stay around until the memory is needed so badly that an OOM would be triggered. TLDR: I think we are 95% of the way to the full solution and the final 5% are either a lot of work or have big drawbacks. |
Might also be a good idea to add a section in the README explaining some of the limitations for the fn-deps/refs. I've been meaning to document all the features of Orchard in the README, but I never got to doing so. We have to start somewhere and that's as good of a place as any. |
I've added some more explanation to the readme and a docstring to the class-cache. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, looking good thanks!
Might give it one last look later today.
Could you update the changelog and PR description in the meantime?
Done. I also wrote a follow-up post on why the class cache is hard to clear and linked that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there 👍 ty!
If you think this is relevant to orchard I'll add a |
src/orchard/xref.clj
Outdated
(Math/min 1 2) | ||
(def vars (q/vars {:ns-query {:project? true} :private? true})) | ||
|
||
(map fn-deps vars)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The final newline seems to be missing.
all-vars (q/vars {:ns-query {:project? true} :private? true}) | ||
deps-map (zipmap all-vars (map fn-deps all-vars))] | ||
(map first (filter (fn [[_k v]] (contains? v var)) deps-map)))) | ||
|
||
(comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are those some functions you used for test purposes? Might be good idea to add a comment explaining this.
Looks good to me overall. I even read the blog post - well done! I added a few small remarks and we are good to go. I guess we'll squash this to a single comment in the final version. |
I've added fn-transitive-deps, if you don't want it in here I'll throw it out again or put it in another PR. |
Co-authored-by: vemv <vemv@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One final note from my side
Cheers 🍻 |
Fixes #51
Uses Clojures internal class cache to also check anonymous function dependencies.