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

fn-deps for lambdas #141

Merged
merged 18 commits into from
Jan 8, 2022
Merged

fn-deps for lambdas #141

merged 18 commits into from
Jan 8, 2022

Conversation

Cyrik
Copy link
Contributor

@Cyrik Cyrik commented Jan 2, 2022

Fixes #51

Uses Clojures internal class cache to also check anonymous function dependencies.

@Cyrik Cyrik changed the title fn-deps for lambdas WIP:fn-deps for lambdas Jan 2, 2022
@vemv vemv self-assigned this Jan 2, 2022
@vemv vemv marked this pull request as draft January 2, 2022 21:53
@vemv vemv removed their assignment Jan 2, 2022
@bbatsov
Copy link
Member

bbatsov commented Jan 2, 2022

Small note - we can't really depend on tools.reader, as this might create conflicts with user applications. Orchard has to have no runtime deps.

@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 2, 2022

Small note - we can't really depend on tools.reader, as this might create conflicts with user applications.

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?

@bbatsov
Copy link
Member

bbatsov commented Jan 3, 2022

The idea itself seems reasonable to me.

@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 5, 2022

I've got a new solution that doesn't need to recompile the code and is a lot faster.
The problem with the new solution is that old classes stay around in clojures cache for some time. I haven't found a way yet to find out which old classes aren't in use anymore.
But the new solution is already a lot better than the old one.
Do you have any ideas on how to fix the final problem?
Also would you want to keep the recompile version as an extra possibility if the actual bytecode is needed for something else?

@vemv
Copy link
Member

vemv commented Jan 5, 2022

The problem with the new solution is that old classes stay around in clojures cache for some time.

Can you explain this as a PR comment (self-review)? That way we can see the code that the problem in question relates to.

(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))
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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 :)

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
@vemv
Copy link
Member

vemv commented Jan 5, 2022

Part of the context is https://lukas-domagala.de/blog/clojure-analysis-and-introspection.html which says:

[clj-kondo] works great but has two new problems:

  • it's kind of slow (40 seconds for that project, including jars)
  • does not work for running processes where you don't have the code (remote repls)

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.

@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 5, 2022

Thank you for your comments @vemv
I completely understand your position. Clj-kondo is great, it's just a little slow for my use case, which it was not meant to handle.

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.

Copy link
Member

@vemv vemv left a 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!

src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
@bbatsov
Copy link
Member

bbatsov commented Jan 5, 2022

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.

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! :-)

src/orchard/xref.clj Outdated Show resolved Hide resolved
@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 6, 2022

I'm still trying to fix the "old Lamdba" reference problem but otherwise this is done.
There is a hacky fix where you force an OOM Exception on purpose, but it doesn't always work and feels very dirty. Hopefully I'll find another way, or I'll leave it like this.

@vemv
Copy link
Member

vemv commented Jan 6, 2022

Kudos for the iteration!

There's System/gc, have you tried that? It might make sense to invoke it whenever the user invokes the functionality (as opposed to on a schedule or something similarly invasive).

WIth WeakReference (vs. SoftReference), GCs are even more effective, but I realise we cannot fork Clojure.

@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 7, 2022

gc does not help, it only clears weakReferences. Softreferences stay around until the memory is needed so badly that an OOM would be triggered.
I've done a few hours of research at this point and could not find any consistent way to clear the old softReferences from the cache. The oom function inside the comment does work sometimes, but it's very hacky and does expand the jvm used memory to the max, so that is not ideal either.
The only other solution I found is JVM TI but using that from clojure would be very annoying and it's not part of every jvm.

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.

@Cyrik Cyrik marked this pull request as ready for review January 7, 2022 00:14
@bbatsov
Copy link
Member

bbatsov commented Jan 7, 2022

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.

@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 7, 2022

I've added some more explanation to the readme and a docstring to the class-cache.
I've linked to my own blog post about the details rather than having too many details in the readme. If you don't like that I can pull out the relevant parts into its own doc file, it might just be too detailed for the main readme.

Copy link
Member

@vemv vemv left a 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?

@Cyrik Cyrik changed the title WIP:fn-deps for lambdas fn-deps for lambdas Jan 7, 2022
@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 7, 2022

Done.

I also wrote a follow-up post on why the class cache is hard to clear and linked that.

Copy link
Member

@vemv vemv left a comment

Choose a reason for hiding this comment

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

Almost there 👍 ty!

README.md Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
src/orchard/xref.clj Outdated Show resolved Hide resolved
@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 8, 2022

If you think this is relevant to orchard I'll add a fn-transitive-deps function to this PR. It would just be a recursive walk over the dependencies, so it might also be ok for users to write that themselfs.

@vemv vemv requested a review from bbatsov January 8, 2022 14:35
test/orchard/xref_test.clj Outdated Show resolved Hide resolved
(Math/min 1 2)
(def vars (q/vars {:ns-query {:project? true} :private? true}))

(map fn-deps vars))
Copy link
Member

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
Copy link
Member

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.

@bbatsov
Copy link
Member

bbatsov commented Jan 8, 2022

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.

@Cyrik
Copy link
Contributor Author

Cyrik commented Jan 8, 2022

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.
Also added one more caveat to the readme. Didn't know this didn't work on AoT code, just found that out because of the transitive test.

test/orchard/xref_test.clj Outdated Show resolved Hide resolved
Co-authored-by: vemv <vemv@users.noreply.github.com>
Copy link
Member

@vemv vemv left a 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

test/orchard/xref_test.clj Outdated Show resolved Hide resolved
@vemv vemv merged commit 63c5800 into clojure-emacs:master Jan 8, 2022
@vemv
Copy link
Member

vemv commented Jan 8, 2022

Cheers 🍻

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.

Implement find-usages
3 participants