Skip to content

Commit edce823

Browse files
Implement config saving
1 parent 9ca5369 commit edce823

File tree

9 files changed

+111
-44
lines changed

9 files changed

+111
-44
lines changed

res/flamegraph/script.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const qString = new URLSearchParams(window.location.search)
1010
const transformFilterTemplate = document.getElementById('transformFilterTemplate');
1111
const transformReplaceTemplate = document.getElementById('transformReplaceTemplate');
1212
const minSamplesToShow = 0; // Don't hide any frames for now.
13+
const profileId = "<<<profileId>>>";
1314

1415
/// Config handling
1516

@@ -223,12 +224,16 @@ async function pasteConfigFromClipboard() {
223224
}
224225

225226
async function saveConfigToServer() {
226-
let packedConfig = await constructPackedConfig();
227-
let req = new XMLHttpRequest();
228-
req.open("POST", "/save-config?packed-config=" + packedConfig);
229-
console.log("[clj-async-profiler] Sending save-config request to backend:", req);
230-
req.send();
231-
showToast("Config saved to server.")
227+
let editToken = window.prompt("Paste editToken here to save the profile config", "");
228+
if (editToken != null && editToken != "") {
229+
let packedConfig = await constructPackedConfig();
230+
let req = new XMLHttpRequest();
231+
req.open("POST", `/api/v1/save-profile-config?id=${profileId}&edit-token=${editToken}&config=${packedConfig}`);
232+
console.log("[clj-async-profiler] Sending save-config request to backend:", req);
233+
req.send();
234+
if (req.status == 204)
235+
showToast("Config saved to server.")
236+
}
232237
}
233238

234239
function match(string, obj) {

res/flamegraph/template.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@
183183
<button id="pasteConfigButton" class="toggle" onclick="pasteConfigFromClipboard()" title="Paste config">
184184
<svg><use href="#paste-icon"/></svg>
185185
</button>
186-
<!-- <button id="saveConfigButton" class="toggle" onclick="saveConfigToServer()" title="Save config"> -->
187-
<!-- <svg><use href="#save-icon"/></svg> -->
188-
<!-- </button> -->
186+
<button id="saveConfigButton" class="toggle" onclick="saveConfigToServer()" title="Save config">
187+
<svg><use href="#save-icon"/></svg>
188+
</button>
189189
</div>
190190
<div class="sidebar-row chip-row">
191191
<button class="chip btn" onclick="addNewTransform('filter')" >+ Filter</button>

res/migrations/202141113-04-stats.up.sql

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/flamebin/core.clj

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
[flamebin.rate-limiter :as rl]
66
[flamebin.render :as render]
77
[flamebin.storage :as storage]
8-
[flamebin.util :refer [raise secret-token new-id]])
8+
[flamebin.util :refer [raise secret-token new-id]]
9+
[taoensso.timbre :as log])
910
(:import java.time.Instant))
1011

1112
(defn- ensure-saved-limits [ip length-kb]
@@ -27,7 +28,7 @@
2728
(storage/save-file dpf-array filename)
2829
;; TODO: replace IP with proper owner at some point
2930
(-> (dto/->Profile id filename (:type params) (:total-samples profile) ip
30-
edit-token public (Instant/now))
31+
edit-token public nil (Instant/now))
3132
db/insert-profile
3233
;; Attach read-token to the response here — it's not in the scheme
3334
;; because we don't store it in the DB.
@@ -42,17 +43,27 @@
4243

4344
(-> (storage/get-file file_path)
4445
(proc/read-compressed-profile read-token)
45-
(render/render-html-flamegraph {}))))
46+
(render/render-html-flamegraph profile {}))))
4647

47-
(defn delete-profile [profile-id provided-edit-token]
48-
(let [{:keys [edit_token file_path]} (db/get-profile profile-id)]
49-
;; Authorization
48+
(defn- authorize-profile-editing [profile provided-edit-token]
49+
(let [{:keys [edit_token]} profile]
5050
(when (and edit_token (not= provided-edit-token edit_token))
51-
(raise 403 "Required edit-token to perform this action."))
52-
(println file_path)
51+
(raise 403 "Required edit-token to perform this action."))))
52+
53+
(defn delete-profile [profile-id provided-edit-token]
54+
(let [{:keys [file_path] :as profile} (db/get-profile profile-id)]
55+
(authorize-profile-editing profile provided-edit-token)
5356
(storage/delete-file file_path)
5457
(db/delete-profile profile-id)))
5558

59+
(defn save-profile-config [profile-id config-string provided-edit-token]
60+
(let [{:keys [file_path] :as profile} (db/get-profile profile-id)]
61+
(authorize-profile-editing profile provided-edit-token)
62+
(when (> (count config-string) 2000)
63+
(raise 413 "Config string is too long."))
64+
(log/infof "Setting config for profile %s: %s" profile-id config-string)
65+
(db/save-profile-config profile-id config-string)))
66+
5667
(defn list-public-profiles []
5768
(db/list-public-profiles 20))
5869

src/flamebin/db.clj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
:lock (ReentrantLock.)}
3232
migrate))
3333

34-
#_(mount/start #'db)
34+
#_(do (mount/stop #'db) (mount/start #'db))
3535

3636
;;;; DB interaction
3737

@@ -73,21 +73,21 @@
7373

7474
(defn list-profiles []
7575
(with-locking (:lock @db)
76-
(->> (jdbc/execute! @db ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, is_public FROM profile"])
76+
(->> (jdbc/execute! @db ["SELECT id, file_path, profile_type, sample_count, owner, config, upload_ts, is_public FROM profile"])
7777
(mapv #(-> (unqualify-keys %)
7878
(assoc :edit_token nil)
7979
(coerce Profile))))))
8080

8181
(defn list-public-profiles [n]
8282
(with-locking (:lock @db)
83-
(->> (jdbc/execute! @db ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, is_public, edit_token FROM profile
83+
(->> (jdbc/execute! @db ["SELECT id, file_path, profile_type, sample_count, owner, config, upload_ts, is_public, edit_token FROM profile
8484
WHERE is_public = 1 ORDER BY upload_ts DESC LIMIT ?" n])
8585
(mapv #(-> (unqualify-keys %)
8686
(coerce Profile))))))
8787

8888
(defn get-profile [profile-id]
8989
(with-locking (:lock @db)
90-
(let [q ["SELECT id, file_path, profile_type, sample_count, owner, upload_ts, edit_token, is_public FROM profile WHERE id = ?" profile-id]
90+
(let [q ["SELECT id, file_path, profile_type, sample_count, owner, config, upload_ts, edit_token, is_public FROM profile WHERE id = ?" profile-id]
9191
row (some-> (jdbc/execute-one! @db q)
9292
unqualify-keys
9393
(coerce Profile))]
@@ -103,6 +103,10 @@ WHERE is_public = 1 ORDER BY upload_ts DESC LIMIT ?" n])
103103
(when (zero? update-count)
104104
(raise 404 (format "Profile with ID '%s' not found." profile-id))))))
105105

106+
(defn save-profile-config [profile-id config]
107+
(with-locking (:lock @db)
108+
(sql-helpers/update! @db :profile {:config config} {:id profile-id})))
109+
106110
(defn clear-db []
107111
(with-locking (:lock @db)
108112
(.delete (clojure.java.io/file (@config :db :path)))

src/flamebin/dto.clj

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,50 @@
1010

1111
(defmacro ^:private defschema-and-constructor [schema-name schema-val]
1212
(assert (= (first schema-val) '->))
13-
(assert (map? (second schema-val)))
14-
(let [ks (keys (second schema-val))]
15-
`(let [sch# ~schema-val]
16-
(def ~schema-name ~schema-val)
17-
(defn ~(symbol (str "->" schema-name)) ~(mapv symbol ks)
18-
(coerce ~(into {} (map #(vector % (symbol %)) ks)) ~schema-name)))))
13+
(assert (= (first (second schema-val)) 'array-map))
14+
(let [ks (mapv first (partition 2 (rest (second schema-val))))]
15+
`(do (def ~schema-name ~schema-val)
16+
(defn ~(symbol (str "->" schema-name)) ~(mapv symbol ks)
17+
(coerce ~(into {} (map #(vector % (symbol %)) ks)) ~schema-name)))))
1918

2019
;;;; Profile
2120

2221
(defschema-and-constructor Profile
23-
(-> {:id :nano-id
22+
(-> (array-map
23+
:id :nano-id
2424
:file_path [:and {:gen/fmap #(str % ".dpf")} :string]
2525
:profile_type :keyword
2626
:sample_count [:maybe nat-int?]
2727
:owner [:maybe :string]
2828
:edit_token [:maybe :string]
2929
:is_public :boolean
30+
:config [:maybe :string]
3031
:upload_ts [:and {:gen/gen (gen/fmap Instant/ofEpochSecond
3132
(gen/choose 1500000000 1700000000))}
32-
:time/instant]}
33+
:time/instant])
3334
mlite/schema))
3435

3536
#_((requiring-resolve 'malli.generator/sample) Profile)
3637

3738
;;;; DenseProfile
3839

3940
(defschema-and-constructor DenseProfile
40-
(-> {:stacks [:vector [:tuple [:vector nat-int?] pos-int?]]
41+
(-> (array-map
42+
:stacks [:vector [:tuple [:vector nat-int?] pos-int?]]
4143
:id->frame [:vector string?]
42-
:total-samples pos-int?}
44+
:total-samples pos-int?)
4345
mlite/schema
4446
(mu/optional-keys [:total-samples])
4547
mu/closed-schema))
4648

4749
;;;; UploadProfileRequest
4850

4951
(defschema-and-constructor UploadProfileRequestParams
50-
(-> {:format [:enum :collapsed :dense-edn]
52+
(-> (array-map
53+
:format [:enum :collapsed :dense-edn]
5154
:kind [:schema {:default :flamegraph} [:enum :flamegraph :diffgraph]]
5255
:type [:re #"[\w\.]+"]
53-
:public [:schema {:default false} :boolean]}
56+
:public [:schema {:default false} :boolean])
5457
mlite/schema))
5558

5659
#_(->UploadProfileRequestParams "collapsed" nil "cpu" true)

src/flamebin/render.clj

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@
44

55
;;;; Flamegraph rendering
66

7-
(defn render-html-flamegraph [dense-profile options]
7+
(defn render-html-flamegraph [dense-profile profile-dto options]
88
(let [{:keys [stacks id->frame]} dense-profile
9+
{:keys [config]} profile-dto
10+
config (if config
11+
(str "\"" config "\"")
12+
"null")
913
idToFrame (#'cljap.render/print-id-to-frame id->frame)
1014
data (#'cljap.render/print-add-stacks stacks false)
1115
user-transforms nil
1216
full-js (-> (slurp (io/resource "flamegraph/script.js"))
1317
(cljap.render/render-template
1418
{:graphTitle (pr-str (or (:title options) ""))
19+
:profileId (:id profile-dto)
1520
:isDiffgraph false
1621
:userTransforms ""
1722
:idToFrame idToFrame
18-
:config "null"
23+
:config config
1924
:stacks data}))]
2025
(-> (slurp (io/resource "flamegraph/template.html"))
2126
(cljap.render/render-template {:script full-js}))))

src/flamebin/web.clj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
(core/delete-profile id edit-token)
5858
(resp 200 {:message (str "Successfully deleted profile: " id)})))
5959

60+
(defn $save-config [{:keys [query-params]}]
61+
(let [{:keys [id edit-token config]} query-params]
62+
(core/save-profile-config id config edit-token)
63+
(resp 204 nil)))
64+
6065
;; Endpoints: web pages
6166

6267
(defn $page-upload-file [req]
@@ -107,6 +112,10 @@
107112
["/upload-profile" {:middleware [wrap-gzip-request]
108113
:post {:handler #'$upload-profile
109114
:parameters {:query' UploadProfileRequestParams}}}]
115+
["/save-profile-config" {:post {:handler #'$save-config
116+
:parameters {:query' {:id :nano-id
117+
:edit-token :string
118+
:config :string}}}}]
110119
;; GET so that user can easily do it in the browser.
111120
["/delete-profile" {:name ::api-delete-profile
112121
:get {:handler #'$delete-profile

test/flamebin/web_test.clj

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,47 @@
144144
serialized-edn)
145145
gzip? gzip-content)})]
146146
(is (match? {:status 201} (dissoc resp :opts)))
147-
(is (match? {:status 200} (req :nil :get (str "/" (:id (:body resp)))))))))
147+
(is (match? {:status 200} (req nil :get (str "/" (:id (:body resp)))))))))
148148

149149
(testing "big files are rejected by the webserver"
150150
(is (match? {:error any?}
151151
(req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu&public=true"
152152
{:body (io/file "test/res/huge.txt")}))))))
153+
154+
(deftest save-profile-config-test
155+
(with-temp :all
156+
(let [resp (req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu&public=true"
157+
{:body (io/file "test/res/small.txt")})
158+
{:keys [id edit_token]} (:body resp)]
159+
(let [conf "H4sIAAAAAAAAE6tWyshMz8jJTM8oUbJSSjJX0lEqKUrMK07LL8otVrKKjq0FALrNy6siAAAA"
160+
resp (req :api :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
161+
id "bad-token" conf))]
162+
(testing "requires edit-token"
163+
(is (match? {:status 403}
164+
(req :api :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
165+
id "bad-token" conf)))))
166+
(testing "rejects big config"
167+
(is (match? {:status 413}
168+
(req :api :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
169+
id edit_token (apply str (repeat 2001 \a)))))))
170+
171+
(let [conf1 "H4sIAAAAAAAAE6tWyshMz8jJTM8oUbJSykjNyclX0lEqzi8q0U2qVLJSykvMTVXSUSopSswrTssvyi1WsoqOrQUA1WAM1jYAAAA="]
172+
(testing "accepts valid config"
173+
(is (match? {:status 204}
174+
(req nil :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
175+
id edit_token conf1))))
176+
177+
(testing "which is then getting baked into flamegraph"
178+
(is (match? {:status 200
179+
:body (re-pattern (format "\nconst bakedPackedConfig = \"%s\"" conf1))}
180+
(req nil :get (format "/%s" id)))))))
181+
182+
(let [conf2 "H4sIAAAAAAAAE6tWKkotSy0qTlWyKikqTdVRKilKzCtOyy_KLVayio6tBQBhhHuhIAAAAA=="]
183+
(testing "swap to another config"
184+
(is (match? {:status 204}
185+
(req nil :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
186+
id edit_token conf2))))
187+
188+
(is (match? {:status 200
189+
:body (re-pattern (format "\nconst bakedPackedConfig = \"%s\"" conf2))}
190+
(req nil :get (format "/%s" id))))))))))

0 commit comments

Comments
 (0)