Skip to content

Commit 71c80f6

Browse files
plexusbbatsov
authored andcommitted
Retain resource URL in java-parser results, improve caching
The java parser/legacy-parser returns a resource's `:file`, actually a relative path as a string, and `:path`, the relative path converted to absolute. It does this conversion through an `io/resource` lookup to find the file system location of the artifact. There are two issues with this: 1. The return value of `io/resource` is discarded, even though it is needed later on in `orchard.java`, causing unnecessary resource lookups. 2. The path is obtained through `(.getPath (io/resource path))`, which yields nonsensical results when used on resources that are inside a JAR. This change keeps the `:file` returned from java-parser (relative path string), but removes the `:path` value in favor of a `:resource-url`, i.e. the return value of `(io/resource path)`. It then provides utility functions to work with this resource URL directly, namely mapping it to a filesystem artifact, or retrieving its modification time. java.parser/parser already does a `io/resource` lookup for the resource it is parsing, meaning it has a full URL (jar: or file:). By including this URL in the map it returns callers can do further checks or operations on the resource without having to re-scan the classpath. This in turn allows us to simplify and optimize `orchard.java/class-info`. The old version would call `io/resource` on each invocation, even when the result was already cached, in order to find the artifact's modification time. This can noticably speed up cider-nrepl's `"stacktrace"` op, which calls `orchard.java/class-info` on each stack frame, in extreme cases taking multiple seconds to analyze all stack frames and return a result. This leads to rather jarring UX, with the stacktrace buffering popping up in Emacs seconds after the evaluation and exception happened. This is exarcerbated when using the nREPL sideloader, which adds overhead to each resource lookup. This also makes the return value of java-parser more correct. As mentioned the previous version would always call `(.getPath (io/resource path))`, but this only makes sense for `file:` resources, not for `jar:` resources. For `file:` URLs `.getPath` returns the path of the file. For `jar:` URLs it returns the nested url+the jar entry, so a `file:` URL but with a dangling `!/jar/entry`. Illustration of why calling `getPath` on `jar:` URLs is not useful: ```clojure (io/resource "lambdaisland/witchcraft.clj") ;;=> #java.net.URL "file:/srv/mc/witchcraft/src/lambdaisland/witchcraft.clj" (.getPath (io/resource "lambdaisland/witchcraft.clj")) ;;=> "/srv/mc/witchcraft/src/lambdaisland/witchcraft.clj" (io/resource "clojure/lang/RT.class") ;;=> #java.net.URL "jar:file:/root/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar!/clojure/lang/RT.class" (.getPath (io/resource "clojure/lang/RT.class")) ;;=> "file:/root/.m2/repository/org/clojure/clojure/1.10.3/clojure-1.10.3.jar!/clojure/lang/RT.class" ```
1 parent 6c9e5d9 commit 71c80f6

File tree

13 files changed

+177
-24
lines changed

13 files changed

+177
-24
lines changed

.clj-kondo/config.edn

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{:output {:progress true
22
:exclude-files ["analysis.cljc" "meta.cljc" "inspect_test.clj"]}
3-
:linters {:unused-private-var {:level :warning :exclude [orchard.query-test/a-private orchard.query-test/docd-fn]}}}
4-
3+
:linters {:unused-private-var {:level :warning :exclude [orchard.query-test/a-private orchard.query-test/docd-fn]}
4+
:refer-all {:exclude [clojure.test]}}}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* [#123](https://github.com/clojure-emacs/orchard/pull/123): Fix info lookups from namespaces that don't yet exist
2222
* [#125](https://github.com/clojure-emacs/orchard/issues/125): Don't fail if the classpath references a non-existing .jar
2323
* [#128](https://github.com/clojure-emacs/orchard/issues/128): Strengthen `apropos`
24+
* [#124](https://github.com/clojure-emacs/orchard/pull/124): Remove costly `io/resource` lookup
2425

2526
## 0.7.1 (2021-04-18)
2627

dev/user.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414

1515
(cond->> ["dev" "src" "test"]
1616
jdk8? (into ["src-jdk8"])
17-
(not jdk8?) (into ["src-newer-jdks"])
17+
(not jdk8?) (into ["src-newer-jdks"
18+
"test-newer-jdks"])
1819
true (apply set-refresh-dirs))

project.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@
8787
"-Dclojure.main.report=stderr"]
8888

8989
:source-paths ["src" "src-jdk8" "src-newer-jdks"]
90+
:test-paths ~(cond-> ["test"]
91+
(not jdk8?)
92+
(conj "test-newer-jdks"))
9093

9194
:profiles {
9295
;; Clojure versions matrix
@@ -96,8 +99,8 @@
9699
[org.clojure/clojure "1.8.0" :classifier "sources"]]}
97100
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]
98101
[org.clojure/clojure "1.9.0" :classifier "sources"]]}
99-
:1.10 {:dependencies [[org.clojure/clojure "1.10.1"]
100-
[org.clojure/clojure "1.10.1" :classifier "sources"]]}
102+
:1.10 {:dependencies [[org.clojure/clojure "1.10.3"]
103+
[org.clojure/clojure "1.10.3" :classifier "sources"]]}
101104
:master {:repositories [["snapshots"
102105
"https://oss.sonatype.org/content/repositories/snapshots"]]
103106
:dependencies [[org.clojure/clojure "1.11.0-master-SNAPSHOT"]

src-jdk8/orchard/java/legacy_parser.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@
273273
(assoc (->> (map parse-info (.classes root))
274274
(filter #(= klass (:class %)))
275275
(first))
276+
;; relative path on the classpath
276277
:file path
277-
:path (. (io/resource path) getPath))))
278+
;; Full URL, e.g. file:.. or jar:...
279+
:resource-url (io/resource path))))
278280
(catch Abort _)))

src-newer-jdks/orchard/java/parser.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,9 @@
304304
(map #(parse-info % root))
305305
(filter #(= klass (:class %)))
306306
(first))
307+
;; relative path on the classpath
307308
:file path
308-
:path (.getPath (io/resource path)))
309+
;; Full URL, e.g. file:.. or jar:...
310+
:resource-url (io/resource path))
309311
(finally (.close (.getJavaFileManager root))))))
310312
(catch Throwable _)))

src/orchard/info.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,7 @@
195195
classes. If no source is available, return the relative path as is."
196196
[^String path]
197197
{:javadoc (java/resolve-javadoc-path path)})
198+
199+
(comment
200+
(info-java 'clojure.lang.RT 'baseLoader)
201+
(file-info "clojure/core.clj"))

src/orchard/java.clj

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,6 @@
250250

251251
(def cache (atom {}))
252252

253-
(defn- immutable-source-file?
254-
"Return true if the source file is effectively immutable. Specifically, this
255-
returns true if no source file is available, or if the source file is in a
256-
jar/zip archive."
257-
[info]
258-
(let [path (:file info)
259-
src (when path (io/resource path))]
260-
(or (not src)
261-
(re-find #"\.(jar|zip)!" (str src)))))
262-
263253
(defn class-info
264254
"For the class symbol, return (possibly cached) Java class and member info.
265255
Members are indexed first by name, and then by argument types to list all
@@ -269,9 +259,11 @@
269259
info (if cached
270260
(:info cached)
271261
(class-info* class))
272-
last-modified (if (immutable-source-file? info)
262+
resource (:resource-url info)
263+
last-modified (if (or (nil? resource)
264+
(util.io/url-to-file-within-archive? resource))
273265
0
274-
(.lastModified ^File (io/file (:path info))))
266+
(util.io/last-modified-time resource))
275267
stale (not= last-modified (:last-modified cached))
276268
;; If last-modified in cache mismatches last-modified of the file,
277269
;; regenerate class-info.

src/orchard/java/resource.clj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
s))
1616

1717
(defn project-resources
18-
"Get a list of classpath resources."
18+
"Get a list of classpath resources, i.e. files that are not clojure/java source
19+
or class files. Only consider classpath entries that are directories, does not
20+
consider jars."
1921
[]
2022
(mapcat
2123
(fn [^File directory]
@@ -32,7 +34,7 @@
3234
{:root directory
3335
:file file
3436
:relpath relpath
35-
:url (io/resource relpath)})))
37+
:url (io/as-url file)})))
3638
(remove #(.startsWith ^String (:relpath %) "META-INF/"))
3739
(remove #(re-matches #".*\.(clj[cs]?|java|class)" (:relpath %)))))
3840
(filter (memfn ^File isDirectory) (map io/as-file (cp/classpath (cp/boot-aware-classloader))))))

src/orchard/util/io.clj

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
(ns orchard.util.io)
1+
(ns orchard.util.io
2+
"Utility functions for dealing with file system objects, and in/out streams."
3+
(:require
4+
[clojure.java.io :as io])
5+
(:import
6+
(java.io File)
7+
(java.net URL JarURLConnection)))
28

39
(defn wrap-silently
410
"Middleware that executes `(f)` without printing to `System/out` or `System/err`.
@@ -22,3 +28,55 @@
2228
(System/setOut old-out))
2329
(when (= ps System/err) ;; `System/err` may have changed in the meantime (in face of concurrency)
2430
(System/setErr old-err)))))))
31+
32+
(defn url-protocol
33+
"Get the URL protocol as a string, e.g. http, file, jar."
34+
[^java.net.URL url]
35+
(.getProtocol url))
36+
37+
(defn url-to-file-within-archive?
38+
"Does this URL point to a file inside a jar (or zip) archive.
39+
i.e. does it use the jar: protocol."
40+
[^java.net.URL url]
41+
(= "jar" (url-protocol url)))
42+
43+
(defn resource-jarfile
44+
"Given a jar:file:...!/... URL, return the location of the jar file on the
45+
filesystem. Returns nil on any other URL."
46+
^File [^URL jar-resource]
47+
(assert (= "jar" (url-protocol jar-resource)))
48+
(let [^JarURLConnection conn (.openConnection jar-resource)
49+
inner-url (.getJarFileURL conn)]
50+
(when (= "file" (url-protocol inner-url))
51+
(io/as-file inner-url))))
52+
53+
(defn resource-artifact
54+
"Return the File from which the given resource URL would be loaded.
55+
56+
For `file:` URLs returns the location of the resource itself, for
57+
`jar:..!/...` URLs returns the location of the archive containing the
58+
resource. Returns a fully qualified File, even when the URL is relative.
59+
Throws when the URL is not a `file:` or `jar:` URL."
60+
^File [^java.net.URL resource]
61+
(let [protocol (url-protocol resource)]
62+
(case protocol
63+
"file"
64+
(io/as-file resource)
65+
"jar"
66+
(resource-jarfile resource)
67+
#_else
68+
(throw (ex-info (str "URLs with a " protocol
69+
" protocol can't be situated on the filesystem.")
70+
{:resource resource})))))
71+
72+
(defprotocol LastModifiedTime
73+
(last-modified-time [this]
74+
"Return the last modified time of a File or resource URL."))
75+
76+
(extend-protocol LastModifiedTime
77+
java.net.URL
78+
(last-modified-time [this]
79+
(last-modified-time (resource-artifact this)))
80+
java.io.File
81+
(last-modified-time [this]
82+
(.lastModified this)))

0 commit comments

Comments
 (0)