├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.clj ├── deps.edn ├── license ├── APACHE_PUBLIC_LICENSE └── ECLIPSE_PUBLIC_LICENSE ├── res └── jamm-0.4.0-cljmm-SNAPSHOT.jar ├── src └── clj_memory_meter │ ├── core.clj │ └── trace.clj └── test └── clj_memory_meter ├── core_test.clj └── trace_test.clj /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | defaults: &defaults 4 | working_directory: ~/project 5 | 6 | executors: 7 | jdk8: 8 | docker: 9 | - image: clojure:temurin-8-noble 10 | <<: *defaults 11 | jdk11: 12 | docker: 13 | - image: clojure:temurin-11-noble 14 | <<: *defaults 15 | jdk17: 16 | docker: 17 | - image: clojure:temurin-17-noble 18 | <<: *defaults 19 | jdk21: 20 | docker: 21 | - image: clojure:temurin-21-noble 22 | <<: *defaults 23 | jdk24: 24 | docker: 25 | - image: clojure:temurin-24-noble 26 | <<: *defaults 27 | 28 | commands: 29 | with_cache: 30 | parameters: 31 | steps: 32 | type: steps 33 | cache_key: 34 | description: Will be used as part of the cache key 35 | type: string 36 | steps: 37 | - run: 38 | name: Generate cache checksum 39 | command: | 40 | for file in deps.edn project.clj 41 | do 42 | find . -name $file -exec cat {} + 43 | done | sha256sum | awk '{print $1}' > /tmp/clojure_cache_seed 44 | - restore_cache: 45 | key: cache-<< parameters.cache_key >>-{{ checksum "/tmp/clojure_cache_seed" }} 46 | - steps: << parameters.steps >> 47 | - save_cache: 48 | paths: 49 | - ~/.m2 50 | - ~/.gitlibs 51 | - .cpcache 52 | key: cache-<< parameters.cache_key >>-{{ checksum "/tmp/clojure_cache_seed" }} 53 | 54 | jobs: 55 | test: 56 | parameters: 57 | jdk_version: 58 | type: string 59 | executor: << parameters.jdk_version >> 60 | steps: 61 | - checkout 62 | - with_cache: 63 | cache_key: "test_v1" 64 | steps: 65 | - run: CLOJURE_VERSION=1.10 clojure -X:1.10:test 66 | - run: CLOJURE_VERSION=1.11 clojure -X:1.11:test 67 | - run: CLOJURE_VERSION=1.12 clojure -X:1.12:test 68 | 69 | deploy: 70 | executor: jdk8 71 | steps: 72 | - checkout 73 | - run: 74 | name: Deploy to Clojars 75 | command: clojure -T:build deploy :version \"$CIRCLE_TAG\" 76 | 77 | run_always: &run_always 78 | filters: 79 | branches: 80 | only: /.*/ 81 | tags: 82 | only: /.*/ 83 | 84 | workflows: 85 | run_all: 86 | jobs: 87 | - test: 88 | matrix: 89 | parameters: 90 | jdk_version: [jdk8, jdk11, jdk17, jdk21, jdk24] 91 | <<: *run_always 92 | - deploy: 93 | requires: 94 | - test 95 | filters: 96 | branches: 97 | ignore: /.*/ 98 | tags: 99 | only: /^\d+\.\d+\.\d+(-\w+)?/ 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .cpcache 3 | /.nrepl-* 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.4.0 (2025-02-18) 4 | 5 | - Add support for [memory 6 | tracing](https://github.com/clojure-goes-fast/clj-memory-meter?tab=readme-ov-file#memory-usage-tracing). 7 | 8 | ### 0.3.0 (2023-05-29) 9 | 10 | - Use newest upstream JAMM version that handles JDK17+ compatibility in a more 11 | sensible way. 12 | - Restore the broken functionality of printing object trees. 13 | 14 | ### 0.2.3 (2023-04-26) 15 | 16 | - Fix performance regression. 17 | 18 | ### 0.2.2 (2023-02-19) 19 | 20 | - Ignore access errors when trying to get private fields with Unsafe (for 21 | example, in hidden classes) and skip the calculation for those fields. 22 | 23 | ### 0.2.1 (2022-07-28) 24 | 25 | - Make Java version detection within JAMM more robust. 26 | 27 | ### 0.2.0 (2022-07-27) 28 | 29 | - [#4](https://github.com/clojure-goes-fast/clj-memory-meter/issues/5): Switch 30 | JAMM dependency to a custom 0.4.0 build that uses Unsafe instead of reflection 31 | when run in JDK 17+. 32 | 33 | ### 0.1.3 (2020-08-11) 34 | 35 | - Update JAMM dependency to 0.3.3. 36 | - [#4](https://github.com/clojure-goes-fast/clj-memory-meter/issues/4): Use proper unit names for power-of-2 sizes. 37 | 38 | ### 0.1.2 (2018-08-28) 39 | 40 | - [#2](https://github.com/clojure-goes-fast/clj-memory-meter/issues/2): Make it possible to use clj-memory-meter outside of REPL environment. 41 | 42 | ### 0.1.0 (2018-03-05) 43 | 44 | - Initial release. 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-memory-meter [![CircleCI](https://img.shields.io/circleci/build/github/clojure-goes-fast/clj-memory-meter/master.svg)](https://dl.circleci.com/status-badge/redirect/gh/clojure-goes-fast/clj-memory-meter/tree/master) ![](https://img.shields.io/badge/dependencies-none-brightgreen) [![](https://img.shields.io/clojars/dt/com.clojure-goes-fast/clj-memory-meter?color=teal)](https://clojars.org/com.clojure-goes-fast/clj-memory-meter) [![](https://img.shields.io/badge/-changelog-blue.svg)](CHANGELOG.md) 2 | 3 | **clj-memory-meter** is a Clojure library that let's you inspect how much memory 4 | an object occupies in the heap. It is a wrapper around [Java Agent for Memory 5 | Measurements](https://github.com/jbellis/jamm). 6 | 7 | Extra features compared to **jamm**: 8 | 9 | 1. Can be added to the project as a simple dependency (don't have to provide a 10 | separate agent file and point to it with a JVM option). 11 | 2. Loadable at runtime. 12 | 3. Human-readable size output. 13 | 4. Memory usage tracing. 14 | 15 | **jamm** JAR file is shipped together with **clj-memory-meter** and unpacked at 16 | runtime. 17 | 18 | ## Usage 19 | 20 | On JDK11 and above, you must start your application with JVM option 21 | `-Djdk.attach.allowAttachSelf`, otherwise the agent will not be able to 22 | dynamically attach to the running process. 23 | 24 | - For Leiningen, add `:jvm-opts ["-Djdk.attach.allowAttachSelf"]` to 25 | `project.clj`. 26 | - For tools.deps, add the same `:jvm-opts` to `deps.edn` or write 27 | `-J-Djdk.attach.allowAttachSelf` explicitly in your REPL command. 28 | 29 | Add `com.clojure-goes-fast/clj-memory-meter` to your dependencies: 30 | 31 | [![](https://clojars.org/com.clojure-goes-fast/clj-memory-meter/latest-version.svg)](https://clojars.org/com.clojure-goes-fast/clj-memory-meter) 32 | 33 | Once loaded, you can measure objects like this: 34 | 35 | ```clojure 36 | (require '[clj-memory-meter.core :as mm]) 37 | 38 | ;; measure calculates total memory occupancy of the object 39 | 40 | (mm/measure "Hello, world!") 41 | ;=> "72 B" 42 | 43 | (mm/measure []) 44 | ;=> "240 B" 45 | 46 | (mm/measure (into {} (map #(vector % (str %)) (range 100)))) 47 | ;=> "9.6 KiB" 48 | 49 | ;; :shallow true calculates only memory occupied by the object itself, 50 | ;; without children 51 | 52 | (mm/measure (object-array (repeatedly 100 #(String. "hello"))) :shallow true) 53 | ;=> "416 B" 54 | (mm/measure (object-array (repeatedly 100 #(String. "hello")))) 55 | ;=> "2.8 KiB" 56 | 57 | ;; :bytes true can be passed to return the size in bytes as a number 58 | 59 | (mm/measure (object-array (repeatedly 100 #(String. "hello"))) :bytes true) 60 | ;=> 2848 61 | 62 | ;; :debug true can be passed to print the object hierarchy. You can also pass an 63 | ;; integer instead of true to limit the number of nested levels printed. 64 | 65 | (mm/measure (apply list (range 4)) :debug true) 66 | 67 | ; root [clojure.lang.PersistentList] 256 bytes (40 bytes) 68 | ; | 69 | ; +--_first [java.lang.Long] 24 bytes (24 bytes) 70 | ; | 71 | ; +--_rest [clojure.lang.PersistentList] 192 bytes (40 bytes) 72 | ; | 73 | ; +--_first [java.lang.Long] 24 bytes (24 bytes) 74 | ; | 75 | ; +--_rest [clojure.lang.PersistentList] 128 bytes (40 bytes) 76 | ; | 77 | ; +--_first [java.lang.Long] 24 bytes (24 bytes) 78 | ; | 79 | ; +--_rest [clojure.lang.PersistentList] 64 bytes (40 bytes) 80 | ; | 81 | ; +--_first [java.lang.Long] 24 bytes (24 bytes) 82 | 83 | ;; Custom MemoryMeter object can be passed. See what you can configure here: 84 | ;; https://github.com/jbellis/jamm/blob/master/src/org/github/jamm/MemoryMeter.java 85 | ``` 86 | 87 | ### Troubleshooting 88 | 89 | Starting with Java 17, JVM no longer allows access to private fields of classes 90 | residing in external modules. On newer Java versions, clj-memory-meter utilizes 91 | Unsafe to get into such private fields. As any Unsafe usage, it can potentially 92 | crash the application. Use at your own risk. 93 | 94 | Because of Unsafe, you may eventually run into errors like this one: 95 | 96 | ``` 97 | Execution error (UnsupportedOperationException) at sun.misc.Unsafe/objectFieldOffset (Unsafe.java:645). 98 | can't get field offset on a hidden class: private final java.util.regex.Pattern$BmpCharPredicate java.util.regex.Pattern$BmpCharPredicate$$Lambda$22/0x80000002c.arg$1 99 | ``` 100 | 101 | The only way to prevent this from happening is to start the REPL with 102 | `--add-opens` JVM options for the in-module private classes. Expand the block 103 | below for the tools.deps alias I use locally for my development REPL. 104 | 105 |
106 | Click to show full :add-opens alias 107 |
:add-opens {:jvm-opts ["--add-opens=java.base/java.io=ALL-UNNAMED"
108 |                        "--add-opens=java.base/java.lang=ALL-UNNAMED"
109 |                        "--add-opens=java.base/java.lang.annotation=ALL-UNNAMED"
110 |                        "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"
111 |                        "--add-opens=java.base/java.lang.module=ALL-UNNAMED"
112 |                        "--add-opens=java.base/java.lang.ref=ALL-UNNAMED"
113 |                        "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"
114 |                        "--add-opens=java.base/java.math=ALL-UNNAMED"
115 |                        "--add-opens=java.base/java.net=ALL-UNNAMED"
116 |                        "--add-opens=java.base/java.net.spi=ALL-UNNAMED"
117 |                        "--add-opens=java.base/java.nio=ALL-UNNAMED"
118 |                        "--add-opens=java.base/java.nio.channels=ALL-UNNAMED"
119 |                        "--add-opens=java.base/java.nio.channels.spi=ALL-UNNAMED"
120 |                        "--add-opens=java.base/java.nio.charset=ALL-UNNAMED"
121 |                        "--add-opens=java.base/java.nio.charset.spi=ALL-UNNAMED"
122 |                        "--add-opens=java.base/java.nio.file=ALL-UNNAMED"
123 |                        "--add-opens=java.base/java.nio.file.attribute=ALL-UNNAMED"
124 |                        "--add-opens=java.base/java.nio.file.spi=ALL-UNNAMED"
125 |                        "--add-opens=java.base/java.security=ALL-UNNAMED"
126 |                        "--add-opens=java.base/java.security.cert=ALL-UNNAMED"
127 |                        "--add-opens=java.base/java.security.interfaces=ALL-UNNAMED"
128 |                        "--add-opens=java.base/java.security.spec=ALL-UNNAMED"
129 |                        "--add-opens=java.base/java.text=ALL-UNNAMED"
130 |                        "--add-opens=java.base/java.text.spi=ALL-UNNAMED"
131 |                        "--add-opens=java.base/java.time=ALL-UNNAMED"
132 |                        "--add-opens=java.base/java.time.chrono=ALL-UNNAMED"
133 |                        "--add-opens=java.base/java.time.format=ALL-UNNAMED"
134 |                        "--add-opens=java.base/java.time.temporal=ALL-UNNAMED"
135 |                        "--add-opens=java.base/java.time.zone=ALL-UNNAMED"
136 |                        "--add-opens=java.base/java.util=ALL-UNNAMED"
137 |                        "--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"
138 |                        "--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED"
139 |                        "--add-opens=java.base/java.util.concurrent.locks=ALL-UNNAMED"
140 |                        "--add-opens=java.base/java.util.function=ALL-UNNAMED"
141 |                        "--add-opens=java.base/java.util.jar=ALL-UNNAMED"
142 |                        "--add-opens=java.base/java.util.regex=ALL-UNNAMED"
143 |                        "--add-opens=java.base/java.util.spi=ALL-UNNAMED"
144 |                        "--add-opens=java.base/java.util.stream=ALL-UNNAMED"
145 |                        "--add-opens=java.base/java.util.zip=ALL-UNNAMED"
146 |                        "--add-opens=java.base/javax.crypto=ALL-UNNAMED"
147 |                        "--add-opens=java.base/javax.crypto.interfaces=ALL-UNNAMED"
148 |                        "--add-opens=java.base/javax.crypto.spec=ALL-UNNAMED"
149 |                        "--add-opens=java.base/javax.net=ALL-UNNAMED"
150 |                        "--add-opens=java.base/javax.net.ssl=ALL-UNNAMED"
151 |                        "--add-opens=java.base/javax.security.auth=ALL-UNNAMED"
152 |                        "--add-opens=java.base/javax.security.auth.callback=ALL-UNNAMED"
153 |                        "--add-opens=java.base/javax.security.auth.login=ALL-UNNAMED"
154 |                        "--add-opens=java.base/javax.security.auth.spi=ALL-UNNAMED"
155 |                        "--add-opens=java.base/javax.security.auth.x500=ALL-UNNAMED"
156 |                        "--add-opens=java.base/javax.security.cert=ALL-UNNAMED"
157 |                        "--add-opens=java.desktop/sun.java2d.marlin=ALL-UNNAMED"
158 |                        "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"
159 |                        "--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"
160 |                        "--add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED"]}
161 |    
162 |
163 | 164 | ## Memory usage tracing 165 | 166 | `clj-memory-meter.trace` provides a way to instrument functions so that they 167 | report heap usage before and after the invocation, and also memory size of the 168 | arguments and return values. This blog post describes why you would want to use 169 | it and how: [Tracking memory usage with 170 | clj-memory-meter.trace](https://clojure-goes-fast.com/blog/tracking-memory-usage/). 171 | 172 | Here's a short example: 173 | 174 | ```clj 175 | (require '[clj-memory-meter.trace :as cmm.trace]) 176 | 177 | (defn make-numbers [n] 178 | (vec (repeatedly n #(rand-int 10000)))) 179 | 180 | (defn avg [numbers] 181 | (/ (reduce + numbers) (count numbers))) 182 | 183 | (defn distribution [m] 184 | (->> (range 1 m) 185 | (mapv #(make-numbers (* 1000000 %))) 186 | (mapv avg))) 187 | 188 | (cmm.trace/trace-var #'make-numbers) 189 | (cmm.trace/trace-var #'avg) 190 | (cmm.trace/trace-var #'distribution) 191 | 192 | (cmm.trace/with-relative-usage 193 | (distribution 5)) 194 | 195 | ; Initial used heap: 63.2 MiB (1.5%) 196 | ; │ (example/distribution <24 B>) | Heap: +472 B (+0.0%) 197 | ; │ │ (example/make-numbers <24 B>) | Heap: +1.5 KiB (+0.0%) 198 | ; │ │ └─→ <20.2 MiB> | Heap: +20.2 MiB (+0.5%) 199 | ; │ │ 200 | ; │ │ (example/make-numbers <24 B>) | Heap: +20.2 MiB (+0.5%) 201 | ; │ │ └─→ <40.5 MiB> | Heap: +60.7 MiB (+1.5%) 202 | ; │ │ 203 | ; │ │ (example/make-numbers <24 B>) | Heap: +60.7 MiB (+1.5%) 204 | ; │ │ └─→ <60.7 MiB> | Heap: +121.6 MiB (+3.0%) 205 | ; │ │ 206 | ; │ │ (example/make-numbers <24 B>) | Heap: +121.6 MiB (+3.0%) 207 | ; │ │ └─→ <80.9 MiB> | Heap: +202.7 MiB (+4.9%) 208 | ; │ │ 209 | ; │ │ (example/avg <20.2 MiB>) | Heap: +230.7 MiB (+5.6%) 210 | ; │ │ └─→ <152 B> | Heap: +202.7 MiB (+4.9%) 211 | ; │ │ 212 | ; │ │ (example/avg <40.5 MiB>) | Heap: +229.7 MiB (+5.6%) 213 | ; │ │ └─→ <152 B> | Heap: +202.7 MiB (+4.9%) 214 | ; │ │ 215 | ; │ │ (example/avg <60.7 MiB>) | Heap: +297.4 MiB (+7.3%) 216 | ; │ │ └─→ <152 B> | Heap: +202.7 MiB (+4.9%) 217 | ; │ │ 218 | ; │ │ (example/avg <80.9 MiB>) | Heap: +295.0 MiB (+7.2%) 219 | ; │ │ └─→ <152 B> | Heap: +202.7 MiB (+4.9%) 220 | ; │ └─→ <864 B> | Heap: +23.3 KiB (+0.0%) 221 | ; Final used heap: +23.2 KiB (+0.0%) 222 | ``` 223 | 224 | Use `trace-var` to instrument a function. You can undo the instrumenting by 225 | calling `untrace-var` on it or redefining the function. Once instrumented, the 226 | function will print the current heap usage before and after its execution. 227 | 228 | By default, the absolute heap usage numbers are printed. But if you wrap the 229 | top-level call in `with-relative-usage` macro (like in the example above), each 230 | heap usage report will be relative to the **beginning** of execution (not 231 | relative to the previous heap report). So, in the example, `Heap: +20.2 MiB` and 232 | then `Heap: +60.7 MiB` means that the heap usage grew by 60 megabytes, not 80. 233 | 234 | If the traced function gets called a lot, it will significantly slow down 235 | execution. **Don't use this in production.** Also, you can disable the following 236 | features to reduce the overhead: 237 | 238 | - `*calculate-argument-and-return-sizes*` — bind to `false` to skip measuring 239 | traced function arguments and returned values; 240 | - `*force-gc-around-traced-functions*` — bind to `false` to avoid calling 241 | `(System/gc)` at traced function boundaries. Note that this will make heap 242 | usage reports not very accurate since they will also include collectable 243 | garbage. 244 | 245 | ## License 246 | 247 | jamm is distributed under Apache-2.0. 248 | See [APACHE_PUBLIC_LICENSE](license/APACHE_PUBLIC_LICENSE) file. The location of the original 249 | repository 250 | is 251 | [https://github.com/jbellis/jamm](https://github.com/jbellis/jamm). 252 | 253 | --- 254 | 255 | clj-memory-meter is distributed under the Eclipse Public License. 256 | See [ECLIPSE_PUBLIC_LICENSE](license/ECLIPSE_PUBLIC_LICENSE). 257 | 258 | Copyright 2018-2025 Oleksandr Yakushev 259 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:refer-clojure :exclude [test]) 3 | (:require [clojure.tools.build.api :as b] 4 | [clojure.tools.build.tasks.write-pom] 5 | [deps-deploy.deps-deploy :as dd])) 6 | 7 | (defn default-opts [{:keys [version] :or {version "99.99"}}] 8 | (let [lib 'com.clojure-goes-fast/clj-memory-meter 9 | url "https://github.com/clojure-goes-fast/clj-memory-meter" 10 | target "target"] 11 | {;; Pom section 12 | :lib lib 13 | :version version 14 | :scm {:url url, :tag version} 15 | :pom-data [[:description "Measure object memory consumption in Clojure"] 16 | [:url url] 17 | [:licenses 18 | [:license 19 | [:name "Eclipse Public License"] 20 | [:url "http://www.eclipse.org/legal/epl-v10.html"]]]] 21 | 22 | ;; Build section 23 | :basis (b/create-basis {}) 24 | :target target 25 | :class-dir (str target "/classes") 26 | :jar-file (some->> version (format "%s/%s-%s.jar" target (name lib) version))})) 27 | 28 | (defmacro defcmd [name args & body] 29 | (assert (= (count args) 1)) 30 | `(defn ~name [~'opts] 31 | (let [~(first args) (merge (default-opts ~'opts) ~'opts)] 32 | ~@body))) 33 | 34 | (defn log [fmt & args] (println (apply format fmt args))) 35 | 36 | (defcmd clean [opts] (b/delete {:path (:target opts)})) 37 | 38 | ;; Hack to propagate scope into pom. 39 | (alter-var-root 40 | #'clojure.tools.build.tasks.write-pom/to-dep 41 | (fn [f] 42 | (fn [[_ {:keys [mvn/scope]} :as arg]] 43 | (let [res (f arg) 44 | alias (some-> res first namespace)] 45 | (cond-> res 46 | (and alias scope) (conj [(keyword alias "scope") scope])))))) 47 | 48 | (defcmd jar [{:keys [class-dir basis jar-file] :as opts}] 49 | (assert (:version opts)) 50 | (doto opts clean b/write-pom) 51 | (log "Building %s..." jar-file) 52 | (b/copy-dir {:src-dirs (:paths basis) 53 | :target-dir class-dir 54 | :include "**"}) 55 | (b/jar opts)) 56 | 57 | (defcmd deploy [{:keys [version jar-file] :as opts}] 58 | (assert (some->> version (re-matches #"\d+\.\d+\.\d+.*")) (str version)) 59 | (jar opts) 60 | (log "Deploying %s to Clojars..." version) 61 | (dd/deploy {:installer :remote 62 | :artifact (b/resolve-path jar-file) 63 | :pom-file (b/pom-path opts)})) 64 | 65 | (defcmd install [{:keys [version] :as opts}] 66 | (jar opts) 67 | (log "Installing %s to local Maven repository..." version) 68 | (b/install opts)) 69 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "res"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0" :mvn/scope "provided"}} 3 | 4 | :aliases 5 | {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.0"} 6 | slipset/deps-deploy {:mvn/version "0.2.2"}} 7 | :ns-default build} 8 | 9 | :1.10 {:override-deps {org.clojure/clojure {:mvn/version "1.10.3"}}} 10 | :1.11 {:override-deps {org.clojure/clojure {:mvn/version "1.11.4"}}} 11 | :1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0"}}} 12 | 13 | :dev {:extra-paths ["test"] 14 | :jvm-opts ["-Djdk.attach.allowAttachSelf"]} 15 | :test {:extra-paths ["test"] 16 | :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" 17 | :git/sha "dfb30dd"} 18 | ;; Dependency only to test the error skipping behavior. 19 | org.spdx/java-spdx-library {:mvn/version "1.1.2"}} 20 | :exec-fn cognitect.test-runner.api/test 21 | :jvm-opts ["-Djdk.attach.allowAttachSelf"]}}} 22 | -------------------------------------------------------------------------------- /license/APACHE_PUBLIC_LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /license/ECLIPSE_PUBLIC_LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /res/jamm-0.4.0-cljmm-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clojure-goes-fast/clj-memory-meter/a9bf36bb25b8f6759917ce1a9e5fa8972dbd1380/res/jamm-0.4.0-cljmm-SNAPSHOT.jar -------------------------------------------------------------------------------- /src/clj_memory_meter/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-memory-meter.core 2 | (:require [clojure.java.io :as io]) 3 | (:import java.io.File 4 | java.lang.management.ManagementFactory 5 | java.net.URLClassLoader)) 6 | 7 | ;;;; Agent JAR unpacking 8 | 9 | (def ^:private jamm-jar-name "jamm-0.4.0-cljmm-SNAPSHOT.jar") 10 | 11 | (defn- unpack-jamm-from-resource [] 12 | (let [dest (File/createTempFile "jamm" ".jar")] 13 | (io/copy (io/input-stream (io/resource jamm-jar-name)) dest) 14 | (.getAbsolutePath dest))) 15 | 16 | (defonce ^:private extracted-jamm-jar (unpack-jamm-from-resource)) 17 | 18 | ;;;; Agent loading 19 | 20 | (defn- tools-jar-url [] 21 | (let [file (io/file (System/getProperty "java.home")) 22 | file (if (.equalsIgnoreCase (.getName file) "jre") 23 | (.getParentFile file) 24 | file) 25 | file (io/file file "lib" "tools.jar")] 26 | (io/as-url file))) 27 | 28 | (defn- add-url-to-classloader-reflective 29 | "This is needed for cases when there is no DynamicClassLoader in the classloader 30 | chain (i.e., the env is not a REPL). Note that this will throw warning on Java 31 | 11 and stops working after Java 17." 32 | [^URLClassLoader loader, url] 33 | (doto (.getDeclaredMethod URLClassLoader "addURL" (into-array Class [java.net.URL])) 34 | (.setAccessible true) 35 | (.invoke loader (object-array [url])))) 36 | 37 | (defn- get-classloader 38 | "Find the uppermost DynamicClassLoader in the chain. However, if the immediate 39 | context classloader is not a DynamicClassLoader, it means we are not run in 40 | the REPL environment, and have to use reflection to patch this classloader. 41 | 42 | Return a tuple of [classloader is-it-dynamic?]." 43 | [] 44 | (let [dynamic-cl? 45 | #(#{"clojure.lang.DynamicClassLoader" "boot.AddableClassLoader"} 46 | (.getName (class %))) 47 | 48 | ctx-loader (.getContextClassLoader (Thread/currentThread))] 49 | (if (dynamic-cl? ctx-loader) 50 | ;; The chain starts with a dynamic classloader, walk the chain up to find 51 | ;; the uppermost one. 52 | (loop [loader ctx-loader] 53 | (let [parent (.getParent loader)] 54 | (if (dynamic-cl? parent) 55 | (recur parent) 56 | [loader true]))) 57 | 58 | ;; Otherwise, return the immediate classloader and tell it's not dynamic. 59 | [ctx-loader false]))) 60 | 61 | (def ^:private tools-jar-classloader 62 | (delay 63 | (let [tools-jar (tools-jar-url) 64 | [loader dynamic?] (get-classloader)] 65 | (if dynamic? 66 | (.addURL loader tools-jar) 67 | (add-url-to-classloader-reflective loader tools-jar)) 68 | loader))) 69 | 70 | (defn- ^Class get-virtualmachine-class [] 71 | ;; In JDK9+, the class is already present, no extra steps required. 72 | (or (try (resolve 'com.sun.tools.attach.VirtualMachine) 73 | (catch ClassNotFoundException _)) 74 | ;; In earlier JDKs, load tools.jar and get the class from there. 75 | (Class/forName "com.sun.tools.attach.VirtualMachine" 76 | false @tools-jar-classloader))) 77 | 78 | (defmacro ^:private get-self-pid* 79 | "This macro expands into proper way of obtaining self PID on JDK9+, and digs 80 | into internals on JDK8." 81 | [] 82 | (if (try (resolve 'java.lang.ProcessHandle) 83 | (catch ClassNotFoundException _)) 84 | `(.pid (java.lang.ProcessHandle/current)) 85 | `(let [runtime# (ManagementFactory/getRuntimeMXBean) 86 | jvm# (.get (doto (.getDeclaredField (class runtime#) "jvm") 87 | (.setAccessible true)) 88 | runtime#)] 89 | (.invoke (doto (.getDeclaredMethod (class jvm#) "getProcessId" 90 | (into-array Class [])) 91 | (.setAccessible true)) 92 | jvm# (object-array []))))) 93 | 94 | (defn- get-self-pid 95 | "Returns the process ID of the current JVM process." 96 | [] 97 | (get-self-pid*)) 98 | 99 | #_(get-self-pid) 100 | 101 | (defn- mk-vm [pid] 102 | (let [vm-class (get-virtualmachine-class) 103 | method (.getDeclaredMethod vm-class "attach" (into-array Class [String]))] 104 | (.invoke method nil (object-array [(str pid)])))) 105 | 106 | (defmacro ^:private load-agent-and-detach 107 | "Call `VirtualMachine.loadAgent` and `VirtualMachine.detach` for the given agent 108 | jar. This macro expands to either reflective or non-reflective call, depending 109 | whether VirtualMachine class is available at compile time (on JDK9+)." 110 | [vm agent-jar] 111 | (let [vm-sym (if (try (resolve 'com.sun.tools.attach.VirtualMachine) 112 | (catch ClassNotFoundException _)) 113 | (with-meta (gensym "vm") {:tag 'com.sun.tools.attach.VirtualMachine}) 114 | (gensym "vm"))] 115 | `(let [~vm-sym ~vm] 116 | (do (.loadAgent ~vm-sym ~agent-jar) 117 | (.detach ~vm-sym))))) 118 | 119 | (def ^:private jamm-agent-loaded 120 | (delay (load-agent-and-detach (mk-vm (get-self-pid)) extracted-jamm-jar) 121 | true)) 122 | 123 | ;;;; Public API 124 | 125 | (defn meter-builder [] 126 | @jamm-agent-loaded 127 | (let [mm-class (Class/forName "org.github.jamm.MemoryMeter") 128 | builder (.getDeclaredMethod mm-class "builder" (into-array Class []))] 129 | (.invoke builder nil (object-array 0)))) 130 | 131 | (defn convert-to-human-readable 132 | "Taken from http://programming.guide/java/formatting-byte-size-to-human-readable-format.html." 133 | [bytes] 134 | (let [unit 1024] 135 | (if (< bytes unit) 136 | (str bytes " B") 137 | (let [exp (int (/ (Math/log bytes) (Math/log unit))) 138 | pre (nth "KMGTPE" (dec exp))] 139 | (format "%.1f %siB" (/ bytes (Math/pow unit exp)) pre))))) 140 | 141 | #_(convert-to-human-readable 512) 142 | #_(convert-to-human-readable 10e8) 143 | 144 | (defn measure 145 | "Measure the memory usage of the `object`. Return a human-readable string. 146 | 147 | :debug - if true, print the object layout tree to stdout. Can also be set to 148 | a number to limit the nesting level being printed. 149 | :shallow - if true, count only the object header and its fields, don't follow 150 | object references 151 | :bytes - if true, return a number of bytes instead of a string 152 | :meter - custom org.github.jamm.MemoryMeter object" 153 | [object & {:keys [debug shallow bytes meter]}] 154 | (let [m (or meter 155 | (let [builder (meter-builder)] 156 | (cond (and debug (boolean? debug)) 157 | (.printVisitedTree builder) 158 | 159 | (integer? debug) 160 | (.printVisitedTreeUpTo builder debug)) 161 | (.build builder))) 162 | byte-count (if shallow 163 | (.measure m object) 164 | (.measureDeep m object))] 165 | (if bytes 166 | byte-count 167 | (convert-to-human-readable byte-count)))) 168 | 169 | #_(measure (vec (repeat 100 "hello")) :debug true) 170 | #_(measure (object-array (repeatedly 1000 (fn [] (String. "foobarbaz")))) :bytes true) 171 | -------------------------------------------------------------------------------- /src/clj_memory_meter/trace.clj: -------------------------------------------------------------------------------- 1 | (ns clj-memory-meter.trace 2 | (:require [clojure.string :as str] 3 | [clj-memory-meter.core :as mm]) 4 | (:import java.lang.management.ManagementFactory)) 5 | 6 | (def ^:private ^:dynamic *depth* 0) 7 | (def ^:private ^:dynamic *initial-used-heap* nil) 8 | 9 | (def ^:dynamic *force-gc-around-traced-functions* 10 | "When true, `System/gc` will be called before invoking a traced function and 11 | after it returns its result. Bind to false if it slows down your evaluation 12 | too much (but this will make used heap measurements inaccurate)." 13 | true) 14 | 15 | (def ^:dynamic *calculate-argument-and-return-sizes* 16 | "When true, `clj-memory-meter.core/measure` will be called on each traced 17 | function's argument and the returned value. Bind to false if it slows down 18 | your evaluation too much." 19 | true) 20 | 21 | (defn- funcall-to-string 22 | "Print the invokation of a functio and size of its arguments to a string." 23 | [fname args outer-prefix] 24 | (format "%s(%s %s)" outer-prefix fname 25 | (if *calculate-argument-and-return-sizes* 26 | (str/join " " (map #(format "<%s>" (mm/measure %)) args)) 27 | "..."))) 28 | 29 | (defn- trace-indent [] 30 | (str/join (repeat *depth* "│ "))) 31 | 32 | (defn- used-heap-bytes [] 33 | (.getUsed (.getHeapMemoryUsage (ManagementFactory/getMemoryMXBean)))) 34 | 35 | (defn- used-heap [] 36 | (let [max-heap-pct (/ (.getMax (.getHeapMemoryUsage 37 | (ManagementFactory/getMemoryMXBean))) 38 | 100.0)] 39 | (if *initial-used-heap* 40 | (let [delta (long (- (used-heap-bytes) *initial-used-heap*)) 41 | sign (if (pos? delta) "+" "-")] 42 | (format "%s%s (%s%.1f%%)" 43 | sign (mm/convert-to-human-readable (Math/abs delta)) 44 | sign (/ (Math/abs delta) max-heap-pct))) 45 | (format "%s (%.1f%%)" 46 | (mm/convert-to-human-readable (used-heap-bytes)) 47 | (/ (used-heap-bytes) max-heap-pct))))) 48 | 49 | (defn- trace-fn-call [name f args] 50 | (println (trace-indent)) 51 | (when *force-gc-around-traced-functions* (System/gc)) 52 | (println (funcall-to-string (str name) args (trace-indent)) 53 | "| Heap:" (used-heap)) 54 | (let [value (binding [*depth* (inc *depth*)] 55 | (apply f args)) 56 | value-size (if *calculate-argument-and-return-sizes* 57 | (str "<" (mm/measure value) ">") 58 | "...")] 59 | (binding [*depth* (inc *depth*)] 60 | (println (trace-indent))) 61 | (when *force-gc-around-traced-functions* (System/gc)) 62 | (println 63 | (format "%s└─→ %s | Heap: %s" (trace-indent) value-size (used-heap))) 64 | value)) 65 | 66 | (defn- relative-usage-entrypoint [f] 67 | (System/gc) 68 | (println "Initial used heap:" (used-heap)) 69 | (binding [*initial-used-heap* (used-heap-bytes) 70 | *depth* (inc *depth*)] 71 | (let [result (f)] 72 | (System/gc) 73 | (println "Final used heap:" (used-heap)) 74 | result))) 75 | 76 | (def ^:private traced-vars (atom #{})) 77 | 78 | (defn- traceable? [v] 79 | (and (var? v) (ifn? @v) (not (:macro (meta v))))) 80 | 81 | ;;;; Public API 82 | 83 | (defn trace-var 84 | "If the specified Var holds an IFn and is not marked as a macro, its 85 | contents is replaced with a version wrapped in a tracing call; 86 | otherwise nothing happens. Can be undone with `untrace-var`." 87 | [v] 88 | (when (and (traceable? v) (not (contains? (meta v) ::traced))) 89 | (let [f @v 90 | vname (symbol v)] 91 | (swap! traced-vars conj v) 92 | (alter-var-root v #(fn tracing-wrapper [& args] 93 | (trace-fn-call vname % args))) 94 | (alter-meta! v assoc ::traced f) 95 | v))) 96 | 97 | (defn untrace-var 98 | "Reverses the effect of `trace-var` for the given Var, replacing the traced 99 | function with the original, untraced version. No-op for non-traced Vars." 100 | [v] 101 | (let [f (::traced (meta v))] 102 | (when f 103 | (alter-var-root v (constantly (::traced (meta v)))) 104 | (alter-meta! v dissoc ::traced) 105 | (swap! traced-vars disj v) 106 | v))) 107 | 108 | (defn untrace-all 109 | "Reverses the effect of tracing for all already traced vars and namespaces." 110 | [] 111 | (run! untrace-var @traced-vars)) 112 | 113 | (defmacro with-relative-usage [& body] 114 | `(#'~`relative-usage-entrypoint (fn [] ~@body))) 115 | -------------------------------------------------------------------------------- /test/clj_memory_meter/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-memory-meter.core-test 2 | (:require [clj-memory-meter.core :as sut] 3 | [clojure.string :as str] 4 | [clojure.test :refer :all]) 5 | (:import org.spdx.library.model.license.LicenseInfoFactory)) 6 | 7 | ;; Sanity check we run the Clojure version which we think we do. 8 | (deftest clojure-version-sanity-check 9 | (is (let [v (System/getenv "CLOJURE_VERSION")] 10 | (println "Running on Clojure" (clojure-version)) 11 | (or (nil? v) (.startsWith (clojure-version) v))))) 12 | 13 | (deftest basic-test 14 | (is (= 240 (sut/measure [] :bytes true))) 15 | (is (= 40 (sut/measure [] :bytes true :shallow true))) 16 | (is (= 48 (sut/measure (java.util.HashMap.) :bytes true))) 17 | (is (= 80 (sut/measure (java.io.File. "/") :bytes true))) 18 | (is (= 24 (sut/measure (java.time.Instant/now) :bytes true)))) 19 | 20 | (defmacro capture-system-out [& body] 21 | `(let [baos# (java.io.ByteArrayOutputStream.) 22 | sw# (java.io.PrintStream. baos#) 23 | bk-out# (System/out)] 24 | (System/setOut sw#) 25 | ~@body 26 | (.flush (System/out)) 27 | (System/setOut bk-out#) 28 | (String. (.toByteArray baos#)))) 29 | 30 | (deftest output-test 31 | (is (= "240 B" (sut/measure []))) 32 | (is (= "3.1 KiB" (sut/measure (vec (range 100))))) 33 | (is (= "2.8 MiB" (sut/measure (vec (range 100000))))) 34 | 35 | (testing ":debug true" 36 | (is (str/ends-with? (str/trim (capture-system-out (sut/measure (apply list (range 4)) :debug true))) 37 | (str/trim " 38 | root [clojure.lang.PersistentList] 256 bytes (40 bytes) 39 | | 40 | +--_first [java.lang.Long] 24 bytes (24 bytes) 41 | | 42 | +--_rest [clojure.lang.PersistentList] 192 bytes (40 bytes) 43 | | 44 | +--_first [java.lang.Long] 24 bytes (24 bytes) 45 | | 46 | +--_rest [clojure.lang.PersistentList] 128 bytes (40 bytes) 47 | | 48 | +--_first [java.lang.Long] 24 bytes (24 bytes) 49 | | 50 | +--_rest [clojure.lang.PersistentList] 64 bytes (40 bytes) 51 | | 52 | +--_first [java.lang.Long] 24 bytes (24 bytes)"))))) 53 | 54 | (deftest error-test 55 | ;; Only test this against JDK14+. 56 | (when (>= (try (eval '(.major (Runtime/version))) (catch Exception _ 0)) 14) 57 | (is (< 100000 (sut/measure (LicenseInfoFactory/getListedLicenseById "Apache-2.0") :bytes true))))) 58 | -------------------------------------------------------------------------------- /test/clj_memory_meter/trace_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-memory-meter.trace-test 2 | (:require [clj-memory-meter.trace :as sut] 3 | clojure.pprint 4 | [clojure.string :as str] 5 | [clojure.test :refer :all]) 6 | (:import java.util.regex.Pattern)) 7 | 8 | (defn ns-fixture 9 | [f] 10 | (in-ns 'clj-memory-meter.trace-test) 11 | (f)) 12 | 13 | ;; Force tests to be run within this namespaces 14 | ;; (see https://github.com/cognitect-labs/test-runner/issues/38) 15 | (use-fixtures :once ns-fixture) 16 | 17 | (defmacro with-out-trimmed-str [& body] 18 | `(let [res# (with-out-str ~@body)] 19 | (->> res# 20 | str/split-lines 21 | (remove str/blank?) 22 | (map str/trim) 23 | (str/join "\n")))) 24 | 25 | (defn =str [s1 s2] 26 | (let [s1s (str/split-lines s1) 27 | s2s (str/split-lines s2) 28 | diff (map (fn [l1 l2] 29 | (when-not 30 | (or (= l2 "**") 31 | (= l1 l2) 32 | (and (str/includes? l2 "**") 33 | (let [parts (map #(Pattern/quote %) (str/split l2 #"\*\*")) 34 | rx (re-pattern (str/join ".*?" parts))] 35 | (re-find rx l1)))) 36 | [l1 l2])) 37 | (str/split-lines s1) 38 | (str/split-lines s2))] 39 | (or (every? nil? diff) 40 | (do (println "FAIL\n:") 41 | (clojure.pprint/pprint diff) 42 | (println "\n-------\ns2:\n") 43 | (clojure.pprint/pprint s2s) 44 | false)))) 45 | 46 | (defn make-numbers [n] 47 | (vec (repeatedly n #(double (rand-int 10000))))) 48 | 49 | (defn avg [numbers] 50 | (/ (reduce + numbers) (count numbers))) 51 | 52 | (defn distribution [m] 53 | (->> (range 1 m) 54 | (mapv #(make-numbers (* 1000000 %))) 55 | (mapv avg))) 56 | 57 | (sut/trace-var #'make-numbers) 58 | (sut/trace-var #'avg) 59 | (sut/trace-var #'distribution) 60 | 61 | (deftest basic-test 62 | (is (=str (with-out-trimmed-str (distribution 5)) 63 | "(clj-memory-meter.trace-test/distribution <24 B>) | Heap: ** (**%) 64 | │ 65 | │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: ** (**%) 66 | │ │ 67 | │ └─→ <28.1 MiB> | Heap: ** (**%) 68 | │ 69 | │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: ** (**%) 70 | │ │ 71 | │ └─→ <56.1 MiB> | Heap: ** (**%) 72 | │ 73 | │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: ** (**%) 74 | │ │ 75 | │ └─→ <84.2 MiB> | Heap: ** (**%) 76 | │ 77 | │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: ** (**%) 78 | │ │ 79 | │ └─→ <112.2 MiB> | Heap: ** (**%) 80 | │ 81 | │ (clj-memory-meter.trace-test/avg <28.1 MiB>) | Heap: ** (**%) 82 | │ │ 83 | │ └─→ <24 B> | Heap: ** (**%) 84 | │ 85 | │ (clj-memory-meter.trace-test/avg <56.1 MiB>) | Heap: ** (**%) 86 | │ │ 87 | │ └─→ <24 B> | Heap: ** (**%) 88 | │ 89 | │ (clj-memory-meter.trace-test/avg <84.2 MiB>) | Heap: ** (**%) 90 | │ │ 91 | │ └─→ <24 B> | Heap: ** (**%) 92 | │ 93 | │ (clj-memory-meter.trace-test/avg <112.2 MiB>) | Heap: ** (**%) 94 | │ │ 95 | │ └─→ <24 B> | Heap: ** (**%) 96 | │ 97 | └─→ <352 B> | Heap: ** (**%) 98 | "))) 99 | 100 | (deftest relative-test 101 | (is (=str (with-out-trimmed-str (sut/with-relative-usage (distribution 5))) 102 | "Initial used heap: ** 103 | │ 104 | │ (clj-memory-meter.trace-test/distribution <24 B>) | Heap: ** (**%) 105 | │ │ 106 | │ │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: ** (**%) 107 | │ │ │ 108 | │ │ └─→ <28.1 MiB> | Heap: +** MiB (+**%) 109 | │ │ 110 | │ │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: +** MiB (+**%) 111 | │ │ │ 112 | │ │ └─→ <56.1 MiB> | Heap: +** MiB (+**%) 113 | │ │ 114 | │ │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: +** MiB (+**%) 115 | │ │ │ 116 | │ │ └─→ <84.2 MiB> | Heap: +** MiB (+**%) 117 | │ │ 118 | │ │ (clj-memory-meter.trace-test/make-numbers <24 B>) | Heap: +** MiB (+**%) 119 | │ │ │ 120 | │ │ └─→ <112.2 MiB> | Heap: +** MiB (+**%) 121 | │ │ 122 | │ │ (clj-memory-meter.trace-test/avg <28.1 MiB>) | Heap: +** MiB (+**%) 123 | │ │ │ 124 | │ │ └─→ <24 B> | Heap: +** MiB (+**%) 125 | │ │ 126 | │ │ (clj-memory-meter.trace-test/avg <56.1 MiB>) | Heap: +** MiB (+**%) 127 | │ │ │ 128 | │ │ └─→ <24 B> | Heap: +** MiB (+**%) 129 | │ │ 130 | │ │ (clj-memory-meter.trace-test/avg <84.2 MiB>) | Heap: +** MiB (+**%) 131 | │ │ │ 132 | │ │ └─→ <24 B> | Heap: +** MiB (+**%) 133 | │ │ 134 | │ │ (clj-memory-meter.trace-test/avg <112.2 MiB>) | Heap: +** MiB (+**%) 135 | │ │ │ 136 | │ │ └─→ <24 B> | Heap: +** MiB (+**%) 137 | │ │ 138 | │ └─→ <352 B> | Heap: ** (**%) 139 | Final used heap: ** (**%)"))) 140 | 141 | (deftest no-args-measurement-test 142 | (is (=str (with-out-trimmed-str 143 | (binding [sut/*calculate-argument-and-return-sizes* false] 144 | (sut/with-relative-usage (distribution 5)))) 145 | "Initial used heap: ** 146 | │ 147 | │ (clj-memory-meter.trace-test/distribution ...) | Heap: ** (**%) 148 | │ │ 149 | │ │ (clj-memory-meter.trace-test/make-numbers ...) | Heap: ** (**%) 150 | │ │ │ 151 | │ │ └─→ ... | Heap: +** MiB (+**%) 152 | │ │ 153 | │ │ (clj-memory-meter.trace-test/make-numbers ...) | Heap: +** MiB (+**%) 154 | │ │ │ 155 | │ │ └─→ ... | Heap: +** MiB (+**%) 156 | │ │ 157 | │ │ (clj-memory-meter.trace-test/make-numbers ...) | Heap: +** MiB (+**%) 158 | │ │ │ 159 | │ │ └─→ ... | Heap: +** MiB (+**%) 160 | │ │ 161 | │ │ (clj-memory-meter.trace-test/make-numbers ...) | Heap: +** MiB (+**%) 162 | │ │ │ 163 | │ │ └─→ ... | Heap: +** MiB (+**%) 164 | │ │ 165 | │ │ (clj-memory-meter.trace-test/avg ...) | Heap: +** MiB (+**%) 166 | │ │ │ 167 | │ │ └─→ ... | Heap: +** MiB (+**%) 168 | │ │ 169 | │ │ (clj-memory-meter.trace-test/avg ...) | Heap: +** MiB (+**%) 170 | │ │ │ 171 | │ │ └─→ ... | Heap: +** MiB (+**%) 172 | │ │ 173 | │ │ (clj-memory-meter.trace-test/avg ...) | Heap: +** MiB (+**%) 174 | │ │ │ 175 | │ │ └─→ ... | Heap: +** MiB (+**%) 176 | │ │ 177 | │ │ (clj-memory-meter.trace-test/avg ...) | Heap: +** MiB (+**%) 178 | │ │ │ 179 | │ │ └─→ ... | Heap: +** MiB (+**%) 180 | │ │ 181 | │ └─→ ... | Heap: ** (**%) 182 | Final used heap: ** (**%)"))) 183 | --------------------------------------------------------------------------------