Skip to content

Commit ab1552b

Browse files
[ui] Redesing profile list page
1 parent 241ac88 commit ab1552b

File tree

4 files changed

+150
-47
lines changed

4 files changed

+150
-47
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### master (unreleased)
44

5+
- New index page design.
6+
57
### 1.3.3 (2024-10-04)
68

79
- [async-profiler#932](https://github.com/async-profiler/async-profiler/issues/923): Fix SIGSEVG crash on JDK23.

res/ui/index.html

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<html>
2+
<head>
3+
<meta charset="utf-8"/>
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<link rel="preconnect" href="https://fonts.googleapis.com">
6+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
7+
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
8+
<link rel='icon' href='favicon.png' type='image/x-icon'/>
9+
<title>clj-async-profiler</title>
10+
<style>
11+
body {margin:1em auto; max-width:800px; background-color:#f9f9f9;}
12+
img {margin:5px}
13+
body, table, input, select {color:#474747; font:0.9em/1.5 Inter, sans-serif;}
14+
table, input, select {background-color:white;}
15+
tbody tr td {border-bottom:1px solid #ededed;}
16+
tbody tr:last-child td {border-bottom: none;}
17+
/* tbody tr {border:1px solid #ededed;} */
18+
tr td, th, th td {padding: 5px 10px;}
19+
th td {font-weight: 500;}
20+
th {text-align: left; padding: 10px;}
21+
thead tr.heading {background-color:white;}
22+
tr.heading td {padding: 1px 10px; font-weight:500; color:black;}
23+
tr.heading {background-color:#F5F5F5}
24+
tr.data td {height:40px}
25+
table th:first-child {border-radius:12px 0 0 0;}
26+
table th:last-child {border-radius:0 12px 0 0;}
27+
th:nth-child(1), td:nth-child(1) {/* max-width:15%; width:15%; */text-align:right;}
28+
/* th:nth-child(2), td:nth-child(2) {max-width:5%; width:5%;} */
29+
/* th:nth-child(6), td:nth-child(6) {max-width:10%; width:10%;} */
30+
a {text-decoration:none;}
31+
table {border-collapse: separate; width:100%; box-shadow: 0 8px 10px 0 rgba(0,0,0,0.2);
32+
border-spacing: 0; border-radius: 10px; border: 1px solid #ededed;
33+
}
34+
.header {justify-content:space-between; margin-bottom:2em;}
35+
.flex-center {display:flex; align-items:center;}
36+
.controls {display:flex; justify-content:center; width:100%;}
37+
input[type=submit] {border: none; padding: 4px 10px; border-radius: 4px;
38+
box-shadow: 2px 2px 3px 0px rgba(0, 0, 0, 0.2);}
39+
select {border: 1px solid #878787; border-radius: 4px; accent-color: green;
40+
padding: 2px;}
41+
</style>
42+
</head>
43+
<body>
44+
<div style="width:100%">
45+
<div class="header flex-center">
46+
<div class="flex-center">
47+
<img width="32px" src="/favicon.png">
48+
<big>clj-async-profiler</big>
49+
</div>
50+
<a href="/clear-results">Clear all results</a>
51+
</div>
52+
<div class="controls"><<<profiler-controls>>></div>
53+
<<<file-table>>>
54+
</div>
55+
</body>
56+
</html>

src/clj_async_profiler/results.clj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@
7272

7373
#_(find-profile (results-file (java.util.Date.) 1 :cpu "flamegraph" "txt"))
7474

75+
(defn find-stacks-file-by-flamegraph-file [^File flamegraph-file]
76+
(if-some [mta (@file->metadata flamegraph-file)]
77+
(:stacks-file mta)
78+
;; If the given flamegraph file has no in-memory metadata, it means the
79+
;; profile was generated in another process. Try to infer the collapsed
80+
;; stacks file from the name of the flamegraph file.
81+
(let [fname (.getName flamegraph-file)
82+
stacks-fname (str (second (re-find #"^(.+)-flamegraph\.html$" fname))
83+
"-collapsed.txt")
84+
stacks (io/file (.getParent flamegraph-file) stacks-fname)]
85+
(when (.exists stacks)
86+
stacks))))
87+
7588
(defn clear-results
7689
"Clear all results from /tmp/clj-async-profiler directory."
7790
[]

src/clj_async_profiler/ui.clj

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
(ns clj-async-profiler.ui
22
(:require [clj-async-profiler.core :as core]
33
[clj-async-profiler.wwws :as wwws :refer [redirect respond]]
4+
[clj-async-profiler.render :as render]
5+
[clj-async-profiler.results :as results]
46
[clojure.java.io :as io]
57
[clojure.string :as str])
6-
(:import java.io.File))
8+
(:import java.io.File
9+
java.util.Date
10+
java.time.format.DateTimeFormatter
11+
java.time.ZoneId))
712

813
(def ^:private ui-state (atom {}))
914

15+
(defmacro ^:private forj {:style/indent 1} [bindings body]
16+
`(str/join (for ~bindings ~body)))
17+
1018
(defn- profiler-controls []
1119
(let [^String status-msg (core/status)]
1220
(if (.contains status-msg "is running")
@@ -16,54 +24,81 @@
1624
(format "<form action=\"/start-profiler\">Event:&nbsp;
1725
<select name=\"event\">%s</select>&nbsp;
1826
<input type=\"submit\" value=\"Start profiler\"/></form>"
19-
(str/join
20-
(for [ev (core/list-event-types {:silent? true})]
21-
(format "<option value=%s>%s</option>" (name ev) (name ev))))))))
22-
23-
(defn- main-page
24-
[root files]
25-
(let [{:keys [show-raw-files]} @ui-state]
26-
(format
27-
"<html><head>
28-
<title>clj-async-profiler</title>
29-
<link rel='icon' href='favicon.png' type='image/x-icon'/>
30-
<style>
31-
body {
32-
margin: 1em auto;
33-
max-width: 40em;
34-
font: 1.1em/1.5 sans-serif;
35-
}
36-
a { text-decoration: none; }
37-
</style></head><body><div><big>clj-async-profiler</big>
38-
<small style=\"float: right\"><a href=\"/toggle-show-raw\">%s</a> | <a href=\"/clear-results\">Clear all results</a></small></div><hr>
39-
<small>%s</small>
40-
<hr><ul>%s</ul><hr></body></html>"
41-
(if show-raw-files "Hide raw files" "Show raw files")
42-
(profiler-controls)
43-
(->> (for [^File f files
44-
:let [fname (.getName f)
45-
sz (.length f)]]
46-
(format "<li><a href='%s'>%s</a> <small><font color=\"gray\">(%s%s)</font></small></li>"
47-
(str root fname)
48-
fname
49-
(if-let [{:keys [samples]} (@@#'core/flamegraph-file->metadata f)]
50-
(if (< samples 1000)
51-
(str samples " samples, ")
52-
(format "%.1fk samples, " (/ samples 1e3)))
53-
"")
54-
(if (< sz 1024) (str sz " b") (format "%.1f Kb" (/ sz 1024.0)))))
55-
str/join))))
27+
(forj [ev (remove #{:ClassName.methodName}
28+
(core/list-event-types {:silent? true}))]
29+
(format "<option value=%s>%s</option>" (name ev) (name ev)))))))
30+
31+
(def ^:private ^DateTimeFormatter date-formatter (DateTimeFormatter/ofPattern "MMM d, yyyy"))
32+
(def ^:private ^DateTimeFormatter time-formatter (DateTimeFormatter/ofPattern "HH:mm"))
33+
34+
(defn- date->local-date [^Date date]
35+
(.format date-formatter (.atZone (.toInstant date) (ZoneId/systemDefault))))
36+
37+
(defn- date->local-time [^Date date]
38+
(.format time-formatter (.atZone (.toInstant date) (ZoneId/systemDefault))))
39+
40+
(defn- row [tag tr-classes & cells]
41+
(let [tag (name tag)
42+
tds (forj [c cells]
43+
(format "<%s>%s</%s>" tag (or c "") tag))]
44+
(format "<tr class='%s'>%s</tr>" tr-classes tds)))
45+
46+
(defn- format-size [sz samples]
47+
(let [sz (if (< sz 1024) (str sz " b") (format "%.1f Kb" (/ sz 1024.0)))
48+
samples (cond (nil? samples) nil
49+
(< samples 1000) (str "<br>" samples " samples")
50+
:else (format "<br>%.1fk samples" (/ samples 1e3)))]
51+
(cond-> sz samples (str samples))))
52+
53+
(defn- file-table [root files]
54+
(let [files (sort-by #(.lastModified ^File %) > files) ;; Newer on top
55+
parsed-files (for [^File f files
56+
:let [info (results/parse-results-filename (.getName f))]]
57+
[f (update info :date #(or % (Date. (.lastModified f))))])
58+
grouped (group-by #(date->local-date (:date (second %))) parsed-files)]
59+
(format
60+
"<table><thead>%s</thead><tbody>%s</tbody></table>"
61+
(row :th "heading"
62+
"Date/time" "ID" "File" "Event" "Size" "Extras" "Actions")
63+
(forj [[local-date day-files] grouped]
64+
(str (row :td "heading"
65+
(str "" local-date "") "" "" "" "" "" "")
66+
(forj [[^File f {:keys [id date kind event]}] day-files
67+
:let [filename (.getName f)
68+
^File stacks (results/find-stacks-file-by-flamegraph-file f)
69+
{:keys [id1 id2 ^File stacks1 ^File stacks2]}
70+
(@results/file->metadata f)]]
71+
(row :td "data"
72+
(date->local-time date)
73+
(when (and id stacks
74+
(= (:stacks-file (results/find-profile id))
75+
stacks))
76+
(format "%02d" id))
77+
(format "<a href='%s'>%s</a>"
78+
(str root filename)
79+
(or kind filename))
80+
(some-> event name)
81+
(str "<small>" (format-size (.length f) (:samples (@results/file->metadata f))) "</small>")
82+
(cond id1 (format "<a href='%s'>%02d</a> vs <a href='%s'>%02d</a>"
83+
(str root (.getName ^File stacks1))
84+
id1
85+
(str root (.getName ^File stacks2))
86+
id2)
87+
stacks (format "<a href='%s'>raw</a>"
88+
(str root (.getName stacks))))
89+
"")))))))
90+
91+
(defn- main-page [root files]
92+
(->> {:profiler-controls (profiler-controls)
93+
:file-table (file-table root files)}
94+
(render/render-template (slurp (io/resource "ui/index.html")))))
5695

5796
(defn- handler [{:keys [path params]} base]
5897
(try
5998
(let [files (->> (.listFiles (io/file base))
6099
(remove #(.isDirectory ^File %))
61100
sort)]
62-
(cond (= path "/toggle-show-raw")
63-
(do (swap! ui-state update :show-raw-files not)
64-
(redirect "/"))
65-
66-
(= path "/start-profiler")
101+
(cond (= path "/start-profiler")
67102
(do (core/start {:event (keyword (params "event"))})
68103
(redirect "/"))
69104

@@ -76,10 +111,7 @@ a { text-decoration: none; }
76111
(redirect "/"))
77112

78113
(= path "/")
79-
(let [files (if (:show-raw-files @ui-state)
80-
files
81-
(remove #(= (wwws/get-extension (str %)) "txt") files))]
82-
{:body (main-page path files)})
114+
{:body (main-page path files)}
83115

84116
(= path "/favicon.png")
85117
{:body (io/resource "favicon.png")

0 commit comments

Comments
 (0)