Skip to content

Commit 601bf2d

Browse files
[flamebin] Implement uploading to Flamebin
1 parent cf4627d commit 601bf2d

File tree

4 files changed

+120
-19
lines changed

4 files changed

+120
-19
lines changed

.circleci/config.yml

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,17 @@ jobs:
6464
parameters:
6565
jdk_version:
6666
type: string
67-
is-alpine:
67+
is_alpine:
68+
type: boolean
69+
default: false
70+
test_flamebin:
6871
type: boolean
6972
default: false
7073
executor: << parameters.jdk_version >>
7174
steps:
7275
- checkout
7376
- when:
74-
condition: << parameters.is-alpine >>
77+
condition: << parameters.is_alpine >>
7578
steps:
7679
- run: apk add --no-cache libstdc++
7780
- with_cache:
@@ -80,7 +83,7 @@ jobs:
8083
- run: clojure -T:build javac
8184
- run: CLOJURE_VERSION=1.10 clojure -X:1.10:test
8285
- run: CLOJURE_VERSION=1.11 clojure -X:1.11:test
83-
- run: CLOJURE_VERSION=1.12 clojure -X:1.12:test
86+
- run: CLOJURE_VERSION=1.12 TEST_FLAMEBIN=<< parameters.test_flamebin >> clojure -X:1.12:test
8487

8588
deploy:
8689
executor: jdk8
@@ -90,36 +93,42 @@ jobs:
9093
name: Deploy
9194
command: clojure -T:build deploy :version \"$CIRCLE_TAG\"
9295

93-
tags_filter: &tags_filter
94-
tags:
95-
only: /^\d+\.\d+\.\d+(-\w+)?/
96+
run_always: &run_always
97+
filters:
98+
branches:
99+
only: /.*/
100+
tags:
101+
only: /.*/
96102

97103
workflows:
98104
run_all:
99105
jobs:
100106
- test:
101107
matrix:
102108
parameters:
103-
jdk_version: [jdk8, jdk11, jdk17, jdk21, jdk23]
104-
filters:
105-
branches:
106-
only: /.*/
107-
<<: *tags_filter
109+
jdk_version: [jdk8, jdk11, jdk17, jdk21]
110+
<<: *run_always
108111
- test:
109112
matrix:
110113
alias: "test-alpine"
111114
parameters:
112115
jdk_version: [jdk8-alpine, jdk21-alpine]
113-
is-alpine: [true]
114-
filters:
115-
branches:
116-
only: /.*/
117-
<<: *tags_filter
116+
is_alpine: [true]
117+
<<: *run_always
118+
- test:
119+
matrix:
120+
alias: "test-with-flamebin"
121+
parameters:
122+
jdk_version: [jdk23]
123+
test_flamebin: [true]
124+
<<: *run_always
118125
- deploy:
119126
requires:
120127
- test
121128
- test-alpine
129+
- test-with-flamebin
122130
filters:
123131
branches:
124132
ignore: /.*/
125-
<<: *tags_filter
133+
tags:
134+
only: /^\d+\.\d+\.\d+(-\w+)?/

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 1.5.0-SNAPSHOT
4+
5+
- Implement uploading flamegraphs to [flamebin.dev](https://flamebin.dev).
6+
37
### 1.4.0 (2024-10-22)
48

59
- New index page design.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
(ns clj-async-profiler.flamebin
2+
"Code responsible for uploading profiling results to flamebin.dev."
3+
(:require [clj-async-profiler.post-processing :as proc]
4+
[clj-async-profiler.results :as results]
5+
[clojure.java.io :as io]
6+
[clojure.pprint :refer [cl-format]])
7+
(:import (java.net HttpURLConnection URL)))
8+
9+
(def flamebin-host (atom "https://flamebin.dev"))
10+
#_(def flamebin-host (atom "http://localhost:8086"))
11+
12+
(defn- make-upload-url [type public?]
13+
(format "%s/api/v1/upload-profile?format=dense-edn&type=%s%s"
14+
@flamebin-host (name type)
15+
(if public? "&public=true" "")))
16+
17+
(defn- gzip-dense-profile [dense-profile]
18+
(let [baos (java.io.ByteArrayOutputStream.)]
19+
(with-open [w (io/writer (java.util.zip.GZIPOutputStream. baos))]
20+
(binding [*print-length* nil
21+
*print-level* nil
22+
*out* w]
23+
(pr dense-profile)))
24+
(.toByteArray baos)))
25+
26+
(defn- report-successful-upload [stacks-file {:keys [location deletion-link read-token]}]
27+
(cl-format *out* "~%~%Uploaded ~A to Flamebin.~%Share URL: ~A~%Deletion URL: ~A~
28+
~@[~%Private uploads don't show on the index page. Private profiles can only be decrypted ~
29+
by providing read-token. The server doesn't store read-token for private uploads.~]"
30+
(str stacks-file) location deletion-link read-token))
31+
32+
(defn- upload-dense-profile [dense-profile event public?]
33+
(let [^bytes gzipped (gzip-dense-profile dense-profile)
34+
url (URL. (make-upload-url event public?))
35+
^HttpURLConnection connection
36+
(doto ^HttpURLConnection (.openConnection url)
37+
(.setDoOutput true)
38+
(.setRequestMethod "POST")
39+
(.setRequestProperty "Content-Type" "application/edn")
40+
(.setRequestProperty "Content-Encoding" "gzip")
41+
(.setRequestProperty "Content-Length" (str (alength gzipped))))]
42+
(with-open [output-stream (.getOutputStream connection)]
43+
(io/copy gzipped output-stream))
44+
(let [status (.getResponseCode connection)
45+
msg (.getResponseMessage connection)
46+
body (slurp (.getInputStream connection))]
47+
(if (< status 400)
48+
(let [location (.getHeaderField connection "Location")
49+
id (.getHeaderField connection "X-Created-ID")
50+
read-token (.getHeaderField connection "X-Read-Token")
51+
edit-token (.getHeaderField connection "X-Edit-Token")
52+
deletion-link (.getHeaderField connection "X-Deletion-Link")]
53+
{:location location, :id id, :body body, :message msg,
54+
:read-token read-token, :edit-token edit-token, :deletion-link deletion-link})
55+
(throw (ex-info (str "Failed to upload profile: " msg)
56+
{:status status, :body body}))))))
57+
58+
(defn upload-to-flamebin
59+
"Generate flamegraph from a collapsed stacks file, identified either by its file
60+
path or numerical ID, and upload it to flamebin.dev. Options:
61+
62+
:public? - if true, flamegraph will be displayed on the main page and publicly
63+
accessible to everyone; otherwise, will requite a token to view."
64+
[run-id-or-stacks-file options]
65+
(let [{:keys [public?]} options
66+
{:keys [stacks-file event]} (results/find-profile run-id-or-stacks-file)
67+
dense-profile (proc/read-raw-profile-file-to-dense-profile stacks-file)
68+
{:keys [location] :as resp} (upload-dense-profile dense-profile event public?)]
69+
(report-successful-upload stacks-file resp)
70+
location))
71+
72+
#_(upload-to-flamebin 1 {:public? true})
73+
#_(upload-to-flamebin "../flamebin/test/res/normal.txt" {})

test/clj_async_profiler/core_test.clj

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
(ns clj-async-profiler.core-test
22
(:require [clj-async-profiler.core :as sut]
3+
[clj-async-profiler.flamebin :as flamebin]
34
[clj-async-profiler.wwws :as wwws]
45
[clj-async-profiler.ui :as ui]
6+
[clojure.string :as str]
57
[clojure.test :refer :all])
68
(:import java.net.URL))
79

@@ -14,15 +16,28 @@
1416
;; Check if agent can attach at all.
1517
(sut/list-event-types)
1618
;; Try profiling a little bit and verify that a file is created.
17-
(sut/start {:event :itimer, :features [:vtable :comptask]})
19+
(sut/start {:event :itimer, :features :all})
1820
(reduce *' (range 1 100000))
1921
(let [stacks-file (sut/stop {:generate-flamegraph? false})]
2022
(is (.exists stacks-file))
2123
(is (> (.length stacks-file) 10000))
2224

2325
(let [fg-file (sut/generate-flamegraph stacks-file {})]
2426
(is (.exists fg-file))
25-
(is (> (.length fg-file) 10000)))))
27+
(is (> (.length fg-file) 10000)))
28+
29+
;; Test uploads to Flamebin
30+
(when (= (System/getenv "TEST_FLAMEBIN") "true")
31+
(reset! flamebin/flamebin-host "https://qa.flamebin.dev")
32+
(flamebin/upload-to-flamebin stacks-file {})
33+
34+
(testing "private (default) uploads contain a read-token"
35+
(is (str/includes? (flamebin/upload-to-flamebin stacks-file {})
36+
"read-token=")))
37+
38+
(testing "public uploads don't have a read-token")
39+
(is (not (str/includes? (flamebin/upload-to-flamebin stacks-file {:public? true})
40+
"read-token="))))))
2641

2742
(defn curl-ui [port]
2843
(let [conn (.openConnection (URL. (str "http://localhost:" port)))]

0 commit comments

Comments
 (0)