Skip to content

Commit 515486a

Browse files
[inspect] Add analytics
1 parent 7df1ada commit 515486a

File tree

5 files changed

+382
-7
lines changed

5 files changed

+382
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## master (unreleased)
44

55
* [#328](https://github.com/clojure-emacs/orchard/pull/328): Inspector: display identity hashcode for Java objects.
6+
* [#329](https://github.com/clojure-emacs/orchard/pull/329): Inspector: add analytics.
67

78
## 0.31.1 (2025-03-19)
89

src/orchard/inspect.clj

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
(:require
1212
[clojure.core.protocols :refer [datafy nav]]
1313
[clojure.string :as str]
14+
[orchard.inspect.analytics :as analytics]
1415
[orchard.print :as print])
1516
(:import
1617
(java.lang.reflect Constructor Field Method Modifier)
@@ -41,15 +42,17 @@
4142
:max-atom-length 150
4243
:max-value-length 10000 ; To avoid printing huge graphs and Exceptions.
4344
:max-coll-size 5
44-
:max-nested-depth nil})
45+
:max-nested-depth nil
46+
:show-analytics-hint nil
47+
:analytics-size-cutoff 100000})
4548

4649
(defn- reset-render-state [inspector]
4750
(-> inspector
4851
(assoc :counter 0, :index [], :indentation 0, :rendered [])
4952
(dissoc :chunk :start-idx :last-page)))
5053

5154
(defn- array? [obj]
52-
(.isArray (class obj)))
55+
(some-> (class obj) .isArray))
5356

5457
(defn- object-type [obj]
5558
(cond
@@ -141,6 +144,7 @@
141144
(assoc :view-mode (peek view-modes-stack))
142145
(update :view-modes-stack pop)
143146
(assoc :value (peek stack))
147+
(dissoc :value-analysis)
144148
(update :stack pop))))
145149

146150
(defn up
@@ -159,6 +163,7 @@
159163
current-page)]
160164
(-> inspector
161165
(assoc :value child)
166+
(dissoc :value-analysis)
162167
(update :stack conj value)
163168
(assoc :current-page 0)
164169
(update :pages-stack conj current-page)
@@ -205,13 +210,16 @@
205210
(sibling* inspector 1))
206211

207212
(defn- validate-config [{:keys [page-size max-atom-length max-value-length
208-
max-coll-size max-nested-depth]
213+
max-coll-size max-nested-depth show-analytics-hint
214+
analytics-size-cutoff]
209215
:as config}]
210216
(when (some? page-size) (pre-ex (pos-int? page-size)))
211217
(when (some? max-atom-length) (pre-ex (pos-int? max-atom-length)))
212218
(when (some? max-value-length) (pre-ex (pos-int? max-value-length)))
213219
(when (some? max-coll-size) (pre-ex (pos-int? max-coll-size)))
214220
(when (some? max-nested-depth) (pre-ex (pos-int? max-nested-depth)))
221+
(when (some? show-analytics-hint) (pre-ex (= show-analytics-hint "true")))
222+
(when (some? analytics-size-cutoff) (pre-ex (pos-int? analytics-size-cutoff)))
215223
(select-keys config (keys default-inspector-config)))
216224

217225
(defn refresh
@@ -257,6 +265,18 @@
257265
(pre-ex (contains? supported-view-modes mode))
258266
(inspect-render (assoc inspector :view-mode mode)))
259267

268+
(defn show-analytics
269+
"Calculates and renders analytics for the current object."
270+
[{:keys [analytics-size-cutoff value] :as inspector}]
271+
(inspect-render
272+
(if (analytics/can-analyze? value)
273+
(-> inspector
274+
(assoc :value-analysis
275+
(binding [analytics/*size-cutoff* analytics-size-cutoff]
276+
(analytics/analytics value)))
277+
(dissoc :show-analytics-hint))
278+
inspector)))
279+
260280
(defn render-onto [inspector coll]
261281
(letfn [(render-one [{:keys [rendered] :as inspector} val]
262282
;; Special case: fuse two last strings together.
@@ -434,6 +454,20 @@
434454
(unindent))
435455
inspector))
436456

457+
(defn- render-analytics
458+
[{:keys [show-analytics-hint value-analysis] :as inspector}]
459+
(if (or value-analysis show-analytics-hint)
460+
(as-> inspector ins
461+
(render-section-header ins "Analytics")
462+
(indent ins)
463+
(if value-analysis
464+
(render-value-maybe-expand ins value-analysis)
465+
(-> ins
466+
(render-indent)
467+
(render-ln "Press 'y' or M-x cider-inspector-show-analytics to analyze this value.")))
468+
(unindent ins))
469+
inspector))
470+
437471
;;;; Datafy
438472

439473
(defn- datafy-kvs [original-object kvs]
@@ -528,6 +562,7 @@
528562
(-> (render-class-name inspector obj)
529563
(render-counted-length obj)
530564
(render-meta-information obj)
565+
(render-analytics)
531566
(render-section-header "Contents")
532567
(indent)
533568
(render-collection-paged)
@@ -850,7 +885,8 @@
850885
(render-view-mode)
851886
(update :rendered seq))))
852887
([inspector value]
853-
(inspect-render (assoc inspector :value value))))
888+
(inspect-render (-> (assoc inspector :value value)
889+
(dissoc :value-analysis)))))
854890

855891
;; Public entrypoints
856892

src/orchard/inspect/analytics.clj

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
(ns orchard.inspect.analytics
2+
"Submodule of Orchard Inspector for getting quick insights about the inspected
3+
data. A \"Metabase\" for Orchard/CIDER Inspector."
4+
(:refer-clojure :exclude [bounded-count])
5+
(:require
6+
[clojure.string :as str])
7+
(:import
8+
(java.util List Map)))
9+
10+
;; To keep execution time under control, only calculate analytics for the first
11+
;; 100k elements.
12+
(def ^:dynamic *size-cutoff* 100000)
13+
14+
(defn- non-nil-hmap [& keyvals]
15+
(->> (partition 2 keyvals)
16+
(keep #(when (some? (second %)) (vec %)))
17+
(into {})))
18+
19+
(defn- *frequencies [coll]
20+
(->> coll
21+
(eduction (take *size-cutoff*))
22+
frequencies
23+
(sort-by second >)
24+
(apply concat)
25+
(apply array-map)))
26+
27+
(definline ^:private inc-if [val condition]
28+
`(cond-> ~val ~condition inc))
29+
30+
(defn- count-pred [pred limit ^Iterable coll]
31+
(let [it (.iterator ^Iterable coll)]
32+
(loop [i 0, n 0]
33+
(if (and (< i limit) (.hasNext it))
34+
(let [x (.next it)]
35+
(recur (inc i) (inc-if n (pred x))))
36+
[n (/ n i)]))))
37+
38+
(defn- bounded-count [limit coll]
39+
(first (count-pred (constantly true) limit coll)))
40+
41+
(defn- list-of-tuples?
42+
"Heuristic-based: an sequence is a list of tuples if at least 20 items of the
43+
first 100, or at least 30% of it, are maps with < 20 values."
44+
[coll]
45+
(and (instance? List coll)
46+
(let [[n ratio] (count-pred #(and (vector? %) (< (count %) 20)) 100 coll)]
47+
(or (> n 20) (> ratio 0.3)))))
48+
49+
(defn- list-of-records?
50+
"Heuristic-based: a sequence is a list of 'records' if at least 20 items of the
51+
first 100, or at least 30% of it, are vectors with size < 20."
52+
[coll]
53+
(and (instance? List coll)
54+
(let [[n ratio] (count-pred #(and (map? %) (< (count %) 20)) 100 coll)]
55+
(or (> n 20) (> ratio 0.3)))))
56+
57+
(defn- numbers-stats [^Iterable coll]
58+
(let [it (.iterator coll)]
59+
(loop [i 0, hi nil, lo nil, zeros 0, n 0, sum 0]
60+
(if (and (< i *size-cutoff*) (.hasNext it))
61+
(let [x (.next it)]
62+
(if (number? x)
63+
(recur (inc i)
64+
(if (nil? hi) x (max hi x))
65+
(if (nil? lo) x (min lo x))
66+
(inc-if zeros (zero? x))
67+
(inc n)
68+
(+ sum x))
69+
(recur (inc i) hi lo zeros n sum)))
70+
(when (> n 0)
71+
{:n n, :zeros zeros, :max hi, :min lo, :mean (float (/ sum n))})))))
72+
73+
(def ^:private ^java.nio.charset.CharsetEncoder ascii-enc
74+
(.newEncoder (java.nio.charset.Charset/forName "US-ASCII")))
75+
76+
(defn- strings-stats [^Iterable coll]
77+
(let [it (.iterator coll)]
78+
(loop [i 0, n 0, blank 0, ascii 0, hi nil, lo nil, sum 0]
79+
(if (and (< i *size-cutoff*) (.hasNext it))
80+
(let [x (.next it)]
81+
(if (string? x)
82+
(let [len (count x)]
83+
(recur (inc i)
84+
(inc n)
85+
(inc-if blank (str/blank? x))
86+
(inc-if ascii (.canEncode ascii-enc ^String x))
87+
(if (nil? hi) len (max hi len))
88+
(if (nil? lo) len (min lo len))
89+
(+ sum len)))
90+
(recur (inc i) n blank ascii hi lo sum)))
91+
(when (> n 0)
92+
{:n n, :blank blank, :ascii ascii, :max-len hi, :min-len lo, :avg-len (float (/ sum n))})))))
93+
94+
(defn- colls-stats [^Iterable coll]
95+
(let [it (.iterator coll)]
96+
(loop [i 0, n 0, empty 0, hi nil, lo nil, sum 0]
97+
(if (and (< i *size-cutoff*) (.hasNext it))
98+
(let [x (.next it)]
99+
(if (instance? java.util.Collection x)
100+
(let [size (count x)]
101+
(recur (inc i)
102+
(inc n)
103+
(inc-if empty (empty? x))
104+
(if (nil? hi) size (max hi size))
105+
(if (nil? lo) size (min lo size))
106+
(+ sum size)))
107+
(recur (inc i) n empty hi lo sum)))
108+
(when (> n 0)
109+
{:n n, :empty empty, :max-size hi, :min-size lo, :avg-size (float (/ sum n))})))))
110+
111+
(defn- basic-list-stats [coll show-count?]
112+
(when (instance? List coll)
113+
(let [cnt (bounded-count *size-cutoff* coll)]
114+
(non-nil-hmap
115+
:cutoff? (when (and show-count? (>= cnt *size-cutoff*)) true)
116+
:count (when show-count? cnt)
117+
:types (*frequencies (map type coll))
118+
:frequencies (*frequencies coll)
119+
:numbers (numbers-stats coll)
120+
:strings (strings-stats coll)
121+
:collections (colls-stats coll)))))
122+
123+
(defn- keyvals-stats [coll]
124+
(when (instance? Map coll)
125+
(let [cnt (bounded-count *size-cutoff* coll)]
126+
(when (> cnt 10)
127+
(non-nil-hmap
128+
:cutoff? (when (>= cnt *size-cutoff*) true)
129+
:count cnt
130+
:keys (basic-list-stats (vec (keys coll)) false)
131+
:values (basic-list-stats (vec (vals coll)) false))))))
132+
133+
(defn- tuples-stats [^Iterable coll]
134+
(when (list-of-tuples? coll)
135+
(let [cnt (bounded-count *size-cutoff* coll)
136+
all (into [] (take *size-cutoff*) coll)
137+
longest (min 20 (apply max (map count all)))]
138+
(non-nil-hmap
139+
:cutoff? (when (>= cnt *size-cutoff*) true)
140+
:count cnt
141+
:tuples (mapv (fn [i]
142+
(basic-list-stats
143+
(mapv #(when (vector? %) (nth % i nil)) all)
144+
false))
145+
(range longest))))))
146+
147+
(defn- records-stats [^Iterable coll]
148+
(when (list-of-records? coll)
149+
(let [cnt (bounded-count *size-cutoff* coll)
150+
ks (set (mapcat keys coll))]
151+
(non-nil-hmap
152+
:cutoff? (when (>= cnt *size-cutoff*) true)
153+
:count cnt
154+
:by-key (into {}
155+
(for [k ks]
156+
(let [kcoll (mapv #(get % k) coll)]
157+
[k (basic-list-stats kcoll false)])))))))
158+
159+
(defn analytics
160+
"Return various analytical data about `object`. Supports the following data
161+
types with different amount of insights:
162+
- lists of numbers
163+
- lists of strings
164+
- lists of tuples
165+
- lists of 'records' (maps with same keys)
166+
- lists of arbitrary collections
167+
- arbitrary key-value maps"
168+
[object]
169+
(or (tuples-stats object)
170+
(records-stats object)
171+
(keyvals-stats object)
172+
(basic-list-stats object true)))
173+
174+
(defn can-analyze?
175+
"Simple heuristic: we currently only analyze collections (but most of them)."
176+
[object]
177+
(instance? java.util.Collection object))

0 commit comments

Comments
 (0)