Skip to content

Commit

Permalink
Implement config saving
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-yakushev committed Nov 30, 2024
1 parent 9ca5369 commit edce823
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 44 deletions.
17 changes: 11 additions & 6 deletions res/flamegraph/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const qString = new URLSearchParams(window.location.search)
const transformFilterTemplate = document.getElementById('transformFilterTemplate');
const transformReplaceTemplate = document.getElementById('transformReplaceTemplate');
const minSamplesToShow = 0; // Don't hide any frames for now.
const profileId = "<<<profileId>>>";

/// Config handling

Expand Down Expand Up @@ -223,12 +224,16 @@ async function pasteConfigFromClipboard() {
}

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

function match(string, obj) {
Expand Down
6 changes: 3 additions & 3 deletions res/flamegraph/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@
<button id="pasteConfigButton" class="toggle" onclick="pasteConfigFromClipboard()" title="Paste config">
<svg><use href="#paste-icon"/></svg>
</button>
<!-- <button id="saveConfigButton" class="toggle" onclick="saveConfigToServer()" title="Save config"> -->
<!-- <svg><use href="#save-icon"/></svg> -->
<!-- </button> -->
<button id="saveConfigButton" class="toggle" onclick="saveConfigToServer()" title="Save config">
<svg><use href="#save-icon"/></svg>
</button>
</div>
<div class="sidebar-row chip-row">
<button class="chip btn" onclick="addNewTransform('filter')" >+ Filter</button>
Expand Down
8 changes: 0 additions & 8 deletions res/migrations/202141113-04-stats.up.sql

This file was deleted.

27 changes: 19 additions & 8 deletions src/flamebin/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
[flamebin.rate-limiter :as rl]
[flamebin.render :as render]
[flamebin.storage :as storage]
[flamebin.util :refer [raise secret-token new-id]])
[flamebin.util :refer [raise secret-token new-id]]
[taoensso.timbre :as log])
(:import java.time.Instant))

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

(-> (storage/get-file file_path)
(proc/read-compressed-profile read-token)
(render/render-html-flamegraph {}))))
(render/render-html-flamegraph profile {}))))

(defn delete-profile [profile-id provided-edit-token]
(let [{:keys [edit_token file_path]} (db/get-profile profile-id)]
;; Authorization
(defn- authorize-profile-editing [profile provided-edit-token]
(let [{:keys [edit_token]} profile]
(when (and edit_token (not= provided-edit-token edit_token))
(raise 403 "Required edit-token to perform this action."))
(println file_path)
(raise 403 "Required edit-token to perform this action."))))

(defn delete-profile [profile-id provided-edit-token]
(let [{:keys [file_path] :as profile} (db/get-profile profile-id)]
(authorize-profile-editing profile provided-edit-token)
(storage/delete-file file_path)
(db/delete-profile profile-id)))

(defn save-profile-config [profile-id config-string provided-edit-token]
(let [{:keys [file_path] :as profile} (db/get-profile profile-id)]
(authorize-profile-editing profile provided-edit-token)
(when (> (count config-string) 2000)
(raise 413 "Config string is too long."))
(log/infof "Setting config for profile %s: %s" profile-id config-string)
(db/save-profile-config profile-id config-string)))

(defn list-public-profiles []
(db/list-public-profiles 20))

Expand Down
12 changes: 8 additions & 4 deletions src/flamebin/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
:lock (ReentrantLock.)}
migrate))

#_(mount/start #'db)
#_(do (mount/stop #'db) (mount/start #'db))

;;;; DB interaction

Expand Down Expand Up @@ -73,21 +73,21 @@

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

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

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

(defn save-profile-config [profile-id config]
(with-locking (:lock @db)
(sql-helpers/update! @db :profile {:config config} {:id profile-id})))

(defn clear-db []
(with-locking (:lock @db)
(.delete (clojure.java.io/file (@config :db :path)))
Expand Down
27 changes: 15 additions & 12 deletions src/flamebin/dto.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,50 @@

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

;;;; Profile

(defschema-and-constructor Profile
(-> {:id :nano-id
(-> (array-map
:id :nano-id
:file_path [:and {:gen/fmap #(str % ".dpf")} :string]
:profile_type :keyword
:sample_count [:maybe nat-int?]
:owner [:maybe :string]
:edit_token [:maybe :string]
:is_public :boolean
:config [:maybe :string]
:upload_ts [:and {:gen/gen (gen/fmap Instant/ofEpochSecond
(gen/choose 1500000000 1700000000))}
:time/instant]}
:time/instant])
mlite/schema))

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

;;;; DenseProfile

(defschema-and-constructor DenseProfile
(-> {:stacks [:vector [:tuple [:vector nat-int?] pos-int?]]
(-> (array-map
:stacks [:vector [:tuple [:vector nat-int?] pos-int?]]
:id->frame [:vector string?]
:total-samples pos-int?}
:total-samples pos-int?)
mlite/schema
(mu/optional-keys [:total-samples])
mu/closed-schema))

;;;; UploadProfileRequest

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

#_(->UploadProfileRequestParams "collapsed" nil "cpu" true)
9 changes: 7 additions & 2 deletions src/flamebin/render.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

;;;; Flamegraph rendering

(defn render-html-flamegraph [dense-profile options]
(defn render-html-flamegraph [dense-profile profile-dto options]
(let [{:keys [stacks id->frame]} dense-profile
{:keys [config]} profile-dto
config (if config
(str "\"" config "\"")
"null")
idToFrame (#'cljap.render/print-id-to-frame id->frame)
data (#'cljap.render/print-add-stacks stacks false)
user-transforms nil
full-js (-> (slurp (io/resource "flamegraph/script.js"))
(cljap.render/render-template
{:graphTitle (pr-str (or (:title options) ""))
:profileId (:id profile-dto)
:isDiffgraph false
:userTransforms ""
:idToFrame idToFrame
:config "null"
:config config
:stacks data}))]
(-> (slurp (io/resource "flamegraph/template.html"))
(cljap.render/render-template {:script full-js}))))
9 changes: 9 additions & 0 deletions src/flamebin/web.clj
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
(core/delete-profile id edit-token)
(resp 200 {:message (str "Successfully deleted profile: " id)})))

(defn $save-config [{:keys [query-params]}]
(let [{:keys [id edit-token config]} query-params]
(core/save-profile-config id config edit-token)
(resp 204 nil)))

;; Endpoints: web pages

(defn $page-upload-file [req]
Expand Down Expand Up @@ -107,6 +112,10 @@
["/upload-profile" {:middleware [wrap-gzip-request]
:post {:handler #'$upload-profile
:parameters {:query' UploadProfileRequestParams}}}]
["/save-profile-config" {:post {:handler #'$save-config
:parameters {:query' {:id :nano-id
:edit-token :string
:config :string}}}}]
;; GET so that user can easily do it in the browser.
["/delete-profile" {:name ::api-delete-profile
:get {:handler #'$delete-profile
Expand Down
40 changes: 39 additions & 1 deletion test/flamebin/web_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,47 @@
serialized-edn)
gzip? gzip-content)})]
(is (match? {:status 201} (dissoc resp :opts)))
(is (match? {:status 200} (req :nil :get (str "/" (:id (:body resp)))))))))
(is (match? {:status 200} (req nil :get (str "/" (:id (:body resp)))))))))

(testing "big files are rejected by the webserver"
(is (match? {:error any?}
(req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu&public=true"
{:body (io/file "test/res/huge.txt")}))))))

(deftest save-profile-config-test
(with-temp :all
(let [resp (req :api :post "/api/v1/upload-profile?format=collapsed&type=cpu&public=true"
{:body (io/file "test/res/small.txt")})
{:keys [id edit_token]} (:body resp)]
(let [conf "H4sIAAAAAAAAE6tWyshMz8jJTM8oUbJSSjJX0lEqKUrMK07LL8otVrKKjq0FALrNy6siAAAA"
resp (req :api :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
id "bad-token" conf))]
(testing "requires edit-token"
(is (match? {:status 403}
(req :api :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
id "bad-token" conf)))))
(testing "rejects big config"
(is (match? {:status 413}
(req :api :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
id edit_token (apply str (repeat 2001 \a)))))))

(let [conf1 "H4sIAAAAAAAAE6tWyshMz8jJTM8oUbJSykjNyclX0lEqzi8q0U2qVLJSykvMTVXSUSopSswrTssvyi1WsoqOrQUA1WAM1jYAAAA="]
(testing "accepts valid config"
(is (match? {:status 204}
(req nil :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
id edit_token conf1))))

(testing "which is then getting baked into flamegraph"
(is (match? {:status 200
:body (re-pattern (format "\nconst bakedPackedConfig = \"%s\"" conf1))}
(req nil :get (format "/%s" id)))))))

(let [conf2 "H4sIAAAAAAAAE6tWKkotSy0qTlWyKikqTdVRKilKzCtOyy_KLVayio6tBQBhhHuhIAAAAA=="]
(testing "swap to another config"
(is (match? {:status 204}
(req nil :post (format "/api/v1/save-profile-config?id=%s&edit-token=%s&config=%s"
id edit_token conf2))))

(is (match? {:status 200
:body (re-pattern (format "\nconst bakedPackedConfig = \"%s\"" conf2))}
(req nil :get (format "/%s" id))))))))))

0 comments on commit edce823

Please sign in to comment.