├── .dockerignore
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .rspec
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── Dockerfile.base
├── Dockerfile.rails_demo
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── build
└── build_kernel_header.sh
├── document
└── safety_argument.md
├── examples
├── aggregrate.rb
├── benchmark.rb
└── demo.rb
├── ext
└── sdb
│ ├── Cargo.toml
│ ├── extconf.rb
│ └── src
│ ├── gvl.rs
│ ├── helpers.rs
│ ├── iseq_logger.rs
│ ├── lib.rs
│ ├── logger.rs
│ ├── ruby_version.rs
│ ├── stack_scanner.rs
│ ├── tester.rs
│ └── trace_id.rs
├── lib
├── sdb.rb
└── sdb
│ ├── puma_patch.rb
│ ├── thread_patch.rb
│ └── version.rb
├── scripts
├── benchmark.js
├── benchmark
│ └── README.md
├── gc_compact.rb
├── handle_thread_reclaim.rb
├── on_cpu_example.rb
├── rails_demo.rb
├── tcp.py
└── thread_schedule.py
├── sdb-shim
├── Cargo.toml
└── src
│ └── lib.rs
├── sdb.gemspec
├── sig
└── sdb.rbs
└── spec
├── ruby_version_spec.rb
└── spec_helper.rb
/.dockerignore:
--------------------------------------------------------------------------------
1 | ./target/
2 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 | push:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | ruby-version: ['3.1.0', '3.1.4', '3.1.6', '3.2.0', '3.2.4', '3.2.6', '3.3.0', '3.3.4', '3.3.6', '3.4.0', '3.4.4']
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Set up Ruby
21 | uses: ruby/setup-ruby@v1
22 | with:
23 | ruby-version: ${{ matrix.ruby-version }}
24 | bundler-cache: true
25 |
26 | - name: Set up Rust
27 | uses: dtolnay/rust-toolchain@stable
28 | with:
29 | toolchain: stable
30 |
31 | - name: Cache Rust dependencies
32 | uses: actions/cache@v3
33 | with:
34 | path: |
35 | ~/.cargo/bin/
36 | ~/.cargo/registry/index/
37 | ~/.cargo/registry/cache/
38 | ~/.cargo/git/db/
39 | target/
40 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
41 | restore-keys: |
42 | ${{ runner.os }}-cargo-
43 |
44 | - name: Install system dependencies
45 | run: |
46 | sudo apt-get update
47 | sudo apt-get install -y build-essential
48 |
49 | - name: Install dependencies
50 | run: bundle install
51 |
52 | - name: Compile extension
53 | run: bundle exec rake compile
54 |
55 | - name: Run tests
56 | run: bundle exec rspec
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | *.bundle
10 | *.so
11 | *.o
12 | *.a
13 | mkmf.log
14 | target/
15 |
16 | # rspec failure tracking
17 | .rspec_status
18 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "1.1.3"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "android-tzdata"
16 | version = "0.1.1"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
19 |
20 | [[package]]
21 | name = "android_system_properties"
22 | version = "0.1.5"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
25 | dependencies = [
26 | "libc",
27 | ]
28 |
29 | [[package]]
30 | name = "autocfg"
31 | version = "1.3.0"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
34 |
35 | [[package]]
36 | name = "bindgen"
37 | version = "0.69.4"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
40 | dependencies = [
41 | "bitflags",
42 | "cexpr",
43 | "clang-sys",
44 | "itertools",
45 | "lazy_static",
46 | "lazycell",
47 | "proc-macro2",
48 | "quote",
49 | "regex",
50 | "rustc-hash",
51 | "shlex",
52 | "syn",
53 | ]
54 |
55 | [[package]]
56 | name = "bitflags"
57 | version = "2.6.0"
58 | source = "registry+https://github.com/rust-lang/crates.io-index"
59 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
60 |
61 | [[package]]
62 | name = "bumpalo"
63 | version = "3.16.0"
64 | source = "registry+https://github.com/rust-lang/crates.io-index"
65 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
66 |
67 | [[package]]
68 | name = "cc"
69 | version = "1.1.15"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
72 | dependencies = [
73 | "shlex",
74 | ]
75 |
76 | [[package]]
77 | name = "cexpr"
78 | version = "0.6.0"
79 | source = "registry+https://github.com/rust-lang/crates.io-index"
80 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
81 | dependencies = [
82 | "nom",
83 | ]
84 |
85 | [[package]]
86 | name = "cfg-if"
87 | version = "1.0.0"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
90 |
91 | [[package]]
92 | name = "chrono"
93 | version = "0.4.38"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
96 | dependencies = [
97 | "android-tzdata",
98 | "iana-time-zone",
99 | "js-sys",
100 | "num-traits",
101 | "wasm-bindgen",
102 | "windows-targets",
103 | ]
104 |
105 | [[package]]
106 | name = "clang-sys"
107 | version = "1.8.1"
108 | source = "registry+https://github.com/rust-lang/crates.io-index"
109 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
110 | dependencies = [
111 | "glob",
112 | "libc",
113 | "libloading",
114 | ]
115 |
116 | [[package]]
117 | name = "core-foundation-sys"
118 | version = "0.8.7"
119 | source = "registry+https://github.com/rust-lang/crates.io-index"
120 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
121 |
122 | [[package]]
123 | name = "crossbeam"
124 | version = "0.8.4"
125 | source = "registry+https://github.com/rust-lang/crates.io-index"
126 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
127 | dependencies = [
128 | "crossbeam-channel",
129 | "crossbeam-deque",
130 | "crossbeam-epoch",
131 | "crossbeam-queue",
132 | "crossbeam-utils",
133 | ]
134 |
135 | [[package]]
136 | name = "crossbeam-channel"
137 | version = "0.5.13"
138 | source = "registry+https://github.com/rust-lang/crates.io-index"
139 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
140 | dependencies = [
141 | "crossbeam-utils",
142 | ]
143 |
144 | [[package]]
145 | name = "crossbeam-deque"
146 | version = "0.8.5"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
149 | dependencies = [
150 | "crossbeam-epoch",
151 | "crossbeam-utils",
152 | ]
153 |
154 | [[package]]
155 | name = "crossbeam-epoch"
156 | version = "0.9.18"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
159 | dependencies = [
160 | "crossbeam-utils",
161 | ]
162 |
163 | [[package]]
164 | name = "crossbeam-queue"
165 | version = "0.3.11"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
168 | dependencies = [
169 | "crossbeam-utils",
170 | ]
171 |
172 | [[package]]
173 | name = "crossbeam-utils"
174 | version = "0.8.20"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
177 |
178 | [[package]]
179 | name = "dark-std"
180 | version = "0.2.16"
181 | source = "registry+https://github.com/rust-lang/crates.io-index"
182 | checksum = "f32345b50634ef57042cbad8d67666efca2809c93f761823b274d6d69de62e8d"
183 | dependencies = [
184 | "flume",
185 | "indexmap",
186 | "parking_lot",
187 | "serde",
188 | ]
189 |
190 | [[package]]
191 | name = "deranged"
192 | version = "0.3.11"
193 | source = "registry+https://github.com/rust-lang/crates.io-index"
194 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
195 | dependencies = [
196 | "powerfmt",
197 | "serde",
198 | ]
199 |
200 | [[package]]
201 | name = "either"
202 | version = "1.13.0"
203 | source = "registry+https://github.com/rust-lang/crates.io-index"
204 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
205 |
206 | [[package]]
207 | name = "equivalent"
208 | version = "1.0.1"
209 | source = "registry+https://github.com/rust-lang/crates.io-index"
210 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
211 |
212 | [[package]]
213 | name = "fast_log"
214 | version = "1.7.3"
215 | source = "registry+https://github.com/rust-lang/crates.io-index"
216 | checksum = "5fb4ef9b3db48f702dfa55a4d28e7c40280a81f4a2ace3e2d82577356fcaedba"
217 | dependencies = [
218 | "crossbeam",
219 | "crossbeam-channel",
220 | "crossbeam-utils",
221 | "dark-std",
222 | "fastdate",
223 | "log",
224 | "once_cell",
225 | "parking_lot",
226 | ]
227 |
228 | [[package]]
229 | name = "fastdate"
230 | version = "0.3.33"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "82bf3c17747b72c0fea787d2a0aa3c54d8fe0bab2f2f8994b3714a6d367bd3a6"
233 | dependencies = [
234 | "libc",
235 | "serde",
236 | "time",
237 | "windows-sys",
238 | ]
239 |
240 | [[package]]
241 | name = "flume"
242 | version = "0.11.0"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
245 | dependencies = [
246 | "futures-core",
247 | "futures-sink",
248 | "spin",
249 | ]
250 |
251 | [[package]]
252 | name = "futures-core"
253 | version = "0.3.30"
254 | source = "registry+https://github.com/rust-lang/crates.io-index"
255 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
256 |
257 | [[package]]
258 | name = "futures-sink"
259 | version = "0.3.30"
260 | source = "registry+https://github.com/rust-lang/crates.io-index"
261 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
262 |
263 | [[package]]
264 | name = "glob"
265 | version = "0.3.1"
266 | source = "registry+https://github.com/rust-lang/crates.io-index"
267 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
268 |
269 | [[package]]
270 | name = "hashbrown"
271 | version = "0.14.5"
272 | source = "registry+https://github.com/rust-lang/crates.io-index"
273 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
274 |
275 | [[package]]
276 | name = "iana-time-zone"
277 | version = "0.1.60"
278 | source = "registry+https://github.com/rust-lang/crates.io-index"
279 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
280 | dependencies = [
281 | "android_system_properties",
282 | "core-foundation-sys",
283 | "iana-time-zone-haiku",
284 | "js-sys",
285 | "wasm-bindgen",
286 | "windows-core 0.52.0",
287 | ]
288 |
289 | [[package]]
290 | name = "iana-time-zone-haiku"
291 | version = "0.1.2"
292 | source = "registry+https://github.com/rust-lang/crates.io-index"
293 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
294 | dependencies = [
295 | "cc",
296 | ]
297 |
298 | [[package]]
299 | name = "indexmap"
300 | version = "2.5.0"
301 | source = "registry+https://github.com/rust-lang/crates.io-index"
302 | checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
303 | dependencies = [
304 | "equivalent",
305 | "hashbrown",
306 | "serde",
307 | ]
308 |
309 | [[package]]
310 | name = "itertools"
311 | version = "0.12.1"
312 | source = "registry+https://github.com/rust-lang/crates.io-index"
313 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
314 | dependencies = [
315 | "either",
316 | ]
317 |
318 | [[package]]
319 | name = "itoa"
320 | version = "1.0.11"
321 | source = "registry+https://github.com/rust-lang/crates.io-index"
322 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
323 |
324 | [[package]]
325 | name = "js-sys"
326 | version = "0.3.70"
327 | source = "registry+https://github.com/rust-lang/crates.io-index"
328 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
329 | dependencies = [
330 | "wasm-bindgen",
331 | ]
332 |
333 | [[package]]
334 | name = "lazy_static"
335 | version = "1.5.0"
336 | source = "registry+https://github.com/rust-lang/crates.io-index"
337 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
338 |
339 | [[package]]
340 | name = "lazycell"
341 | version = "1.3.0"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
344 |
345 | [[package]]
346 | name = "libc"
347 | version = "0.2.169"
348 | source = "registry+https://github.com/rust-lang/crates.io-index"
349 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
350 |
351 | [[package]]
352 | name = "libloading"
353 | version = "0.8.5"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
356 | dependencies = [
357 | "cfg-if",
358 | "windows-targets",
359 | ]
360 |
361 | [[package]]
362 | name = "lock_api"
363 | version = "0.4.12"
364 | source = "registry+https://github.com/rust-lang/crates.io-index"
365 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
366 | dependencies = [
367 | "autocfg",
368 | "scopeguard",
369 | ]
370 |
371 | [[package]]
372 | name = "log"
373 | version = "0.4.22"
374 | source = "registry+https://github.com/rust-lang/crates.io-index"
375 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
376 |
377 | [[package]]
378 | name = "memchr"
379 | version = "2.7.4"
380 | source = "registry+https://github.com/rust-lang/crates.io-index"
381 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
382 |
383 | [[package]]
384 | name = "minimal-lexical"
385 | version = "0.2.1"
386 | source = "registry+https://github.com/rust-lang/crates.io-index"
387 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
388 |
389 | [[package]]
390 | name = "nom"
391 | version = "7.1.3"
392 | source = "registry+https://github.com/rust-lang/crates.io-index"
393 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
394 | dependencies = [
395 | "memchr",
396 | "minimal-lexical",
397 | ]
398 |
399 | [[package]]
400 | name = "ntapi"
401 | version = "0.4.1"
402 | source = "registry+https://github.com/rust-lang/crates.io-index"
403 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
404 | dependencies = [
405 | "winapi",
406 | ]
407 |
408 | [[package]]
409 | name = "num-conv"
410 | version = "0.1.0"
411 | source = "registry+https://github.com/rust-lang/crates.io-index"
412 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
413 |
414 | [[package]]
415 | name = "num-traits"
416 | version = "0.2.19"
417 | source = "registry+https://github.com/rust-lang/crates.io-index"
418 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
419 | dependencies = [
420 | "autocfg",
421 | ]
422 |
423 | [[package]]
424 | name = "once_cell"
425 | version = "1.19.0"
426 | source = "registry+https://github.com/rust-lang/crates.io-index"
427 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
428 |
429 | [[package]]
430 | name = "parking_lot"
431 | version = "0.12.3"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
434 | dependencies = [
435 | "lock_api",
436 | "parking_lot_core",
437 | ]
438 |
439 | [[package]]
440 | name = "parking_lot_core"
441 | version = "0.9.10"
442 | source = "registry+https://github.com/rust-lang/crates.io-index"
443 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
444 | dependencies = [
445 | "cfg-if",
446 | "libc",
447 | "redox_syscall",
448 | "smallvec",
449 | "windows-targets",
450 | ]
451 |
452 | [[package]]
453 | name = "powerfmt"
454 | version = "0.2.0"
455 | source = "registry+https://github.com/rust-lang/crates.io-index"
456 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
457 |
458 | [[package]]
459 | name = "proc-macro2"
460 | version = "1.0.86"
461 | source = "registry+https://github.com/rust-lang/crates.io-index"
462 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
463 | dependencies = [
464 | "unicode-ident",
465 | ]
466 |
467 | [[package]]
468 | name = "quote"
469 | version = "1.0.37"
470 | source = "registry+https://github.com/rust-lang/crates.io-index"
471 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
472 | dependencies = [
473 | "proc-macro2",
474 | ]
475 |
476 | [[package]]
477 | name = "rayon"
478 | version = "1.10.0"
479 | source = "registry+https://github.com/rust-lang/crates.io-index"
480 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
481 | dependencies = [
482 | "either",
483 | "rayon-core",
484 | ]
485 |
486 | [[package]]
487 | name = "rayon-core"
488 | version = "1.12.1"
489 | source = "registry+https://github.com/rust-lang/crates.io-index"
490 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
491 | dependencies = [
492 | "crossbeam-deque",
493 | "crossbeam-utils",
494 | ]
495 |
496 | [[package]]
497 | name = "rb-sys"
498 | version = "0.9.102"
499 | source = "registry+https://github.com/rust-lang/crates.io-index"
500 | checksum = "df4dec4b1d304c3b308a2cd86b1216ea45dd4361f4e9fa056f108332d0a450c1"
501 | dependencies = [
502 | "rb-sys-build",
503 | ]
504 |
505 | [[package]]
506 | name = "rb-sys-build"
507 | version = "0.9.102"
508 | source = "registry+https://github.com/rust-lang/crates.io-index"
509 | checksum = "1d71de3e29d174b8fb17b5d4470f27d7aa2605f8a9d05fda0d3aeff30e05a570"
510 | dependencies = [
511 | "bindgen",
512 | "lazy_static",
513 | "proc-macro2",
514 | "quote",
515 | "regex",
516 | "shell-words",
517 | "syn",
518 | ]
519 |
520 | [[package]]
521 | name = "rbspy-ruby-structs"
522 | version = "0.34.1"
523 | source = "registry+https://github.com/rust-lang/crates.io-index"
524 | checksum = "4a9df8c1d6efcff3b6d60fd5486744319d9f672a8ea9c73ff9c84834eab30ef8"
525 |
526 | [[package]]
527 | name = "redox_syscall"
528 | version = "0.5.3"
529 | source = "registry+https://github.com/rust-lang/crates.io-index"
530 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
531 | dependencies = [
532 | "bitflags",
533 | ]
534 |
535 | [[package]]
536 | name = "regex"
537 | version = "1.10.6"
538 | source = "registry+https://github.com/rust-lang/crates.io-index"
539 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
540 | dependencies = [
541 | "aho-corasick",
542 | "memchr",
543 | "regex-automata",
544 | "regex-syntax",
545 | ]
546 |
547 | [[package]]
548 | name = "regex-automata"
549 | version = "0.4.7"
550 | source = "registry+https://github.com/rust-lang/crates.io-index"
551 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
552 | dependencies = [
553 | "aho-corasick",
554 | "memchr",
555 | "regex-syntax",
556 | ]
557 |
558 | [[package]]
559 | name = "regex-syntax"
560 | version = "0.8.4"
561 | source = "registry+https://github.com/rust-lang/crates.io-index"
562 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
563 |
564 | [[package]]
565 | name = "rustc-hash"
566 | version = "1.1.0"
567 | source = "registry+https://github.com/rust-lang/crates.io-index"
568 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
569 |
570 | [[package]]
571 | name = "ryu"
572 | version = "1.0.18"
573 | source = "registry+https://github.com/rust-lang/crates.io-index"
574 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
575 |
576 | [[package]]
577 | name = "scopeguard"
578 | version = "1.2.0"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
581 |
582 | [[package]]
583 | name = "sdb"
584 | version = "0.1.0"
585 | dependencies = [
586 | "chrono",
587 | "fast_log",
588 | "lazy_static",
589 | "libc",
590 | "log",
591 | "rb-sys",
592 | "rbspy-ruby-structs",
593 | "serde",
594 | "serde_json",
595 | "spin",
596 | "sysinfo",
597 | ]
598 |
599 | [[package]]
600 | name = "sdb-shim"
601 | version = "0.1.0"
602 | dependencies = [
603 | "fast_log",
604 | "libc",
605 | "libloading",
606 | "log",
607 | ]
608 |
609 | [[package]]
610 | name = "serde"
611 | version = "1.0.209"
612 | source = "registry+https://github.com/rust-lang/crates.io-index"
613 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
614 | dependencies = [
615 | "serde_derive",
616 | ]
617 |
618 | [[package]]
619 | name = "serde_derive"
620 | version = "1.0.209"
621 | source = "registry+https://github.com/rust-lang/crates.io-index"
622 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
623 | dependencies = [
624 | "proc-macro2",
625 | "quote",
626 | "syn",
627 | ]
628 |
629 | [[package]]
630 | name = "serde_json"
631 | version = "1.0.127"
632 | source = "registry+https://github.com/rust-lang/crates.io-index"
633 | checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
634 | dependencies = [
635 | "itoa",
636 | "memchr",
637 | "ryu",
638 | "serde",
639 | ]
640 |
641 | [[package]]
642 | name = "shell-words"
643 | version = "1.1.0"
644 | source = "registry+https://github.com/rust-lang/crates.io-index"
645 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
646 |
647 | [[package]]
648 | name = "shlex"
649 | version = "1.3.0"
650 | source = "registry+https://github.com/rust-lang/crates.io-index"
651 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
652 |
653 | [[package]]
654 | name = "smallvec"
655 | version = "1.13.2"
656 | source = "registry+https://github.com/rust-lang/crates.io-index"
657 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
658 |
659 | [[package]]
660 | name = "spin"
661 | version = "0.9.8"
662 | source = "registry+https://github.com/rust-lang/crates.io-index"
663 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
664 | dependencies = [
665 | "lock_api",
666 | ]
667 |
668 | [[package]]
669 | name = "syn"
670 | version = "2.0.77"
671 | source = "registry+https://github.com/rust-lang/crates.io-index"
672 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
673 | dependencies = [
674 | "proc-macro2",
675 | "quote",
676 | "unicode-ident",
677 | ]
678 |
679 | [[package]]
680 | name = "sysinfo"
681 | version = "0.33.1"
682 | source = "registry+https://github.com/rust-lang/crates.io-index"
683 | checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01"
684 | dependencies = [
685 | "core-foundation-sys",
686 | "libc",
687 | "memchr",
688 | "ntapi",
689 | "rayon",
690 | "windows",
691 | ]
692 |
693 | [[package]]
694 | name = "time"
695 | version = "0.3.36"
696 | source = "registry+https://github.com/rust-lang/crates.io-index"
697 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
698 | dependencies = [
699 | "deranged",
700 | "itoa",
701 | "num-conv",
702 | "powerfmt",
703 | "serde",
704 | "time-core",
705 | "time-macros",
706 | ]
707 |
708 | [[package]]
709 | name = "time-core"
710 | version = "0.1.2"
711 | source = "registry+https://github.com/rust-lang/crates.io-index"
712 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
713 |
714 | [[package]]
715 | name = "time-macros"
716 | version = "0.2.18"
717 | source = "registry+https://github.com/rust-lang/crates.io-index"
718 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
719 | dependencies = [
720 | "num-conv",
721 | "time-core",
722 | ]
723 |
724 | [[package]]
725 | name = "unicode-ident"
726 | version = "1.0.12"
727 | source = "registry+https://github.com/rust-lang/crates.io-index"
728 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
729 |
730 | [[package]]
731 | name = "wasm-bindgen"
732 | version = "0.2.93"
733 | source = "registry+https://github.com/rust-lang/crates.io-index"
734 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
735 | dependencies = [
736 | "cfg-if",
737 | "once_cell",
738 | "wasm-bindgen-macro",
739 | ]
740 |
741 | [[package]]
742 | name = "wasm-bindgen-backend"
743 | version = "0.2.93"
744 | source = "registry+https://github.com/rust-lang/crates.io-index"
745 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
746 | dependencies = [
747 | "bumpalo",
748 | "log",
749 | "once_cell",
750 | "proc-macro2",
751 | "quote",
752 | "syn",
753 | "wasm-bindgen-shared",
754 | ]
755 |
756 | [[package]]
757 | name = "wasm-bindgen-macro"
758 | version = "0.2.93"
759 | source = "registry+https://github.com/rust-lang/crates.io-index"
760 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
761 | dependencies = [
762 | "quote",
763 | "wasm-bindgen-macro-support",
764 | ]
765 |
766 | [[package]]
767 | name = "wasm-bindgen-macro-support"
768 | version = "0.2.93"
769 | source = "registry+https://github.com/rust-lang/crates.io-index"
770 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
771 | dependencies = [
772 | "proc-macro2",
773 | "quote",
774 | "syn",
775 | "wasm-bindgen-backend",
776 | "wasm-bindgen-shared",
777 | ]
778 |
779 | [[package]]
780 | name = "wasm-bindgen-shared"
781 | version = "0.2.93"
782 | source = "registry+https://github.com/rust-lang/crates.io-index"
783 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
784 |
785 | [[package]]
786 | name = "winapi"
787 | version = "0.3.9"
788 | source = "registry+https://github.com/rust-lang/crates.io-index"
789 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
790 | dependencies = [
791 | "winapi-i686-pc-windows-gnu",
792 | "winapi-x86_64-pc-windows-gnu",
793 | ]
794 |
795 | [[package]]
796 | name = "winapi-i686-pc-windows-gnu"
797 | version = "0.4.0"
798 | source = "registry+https://github.com/rust-lang/crates.io-index"
799 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
800 |
801 | [[package]]
802 | name = "winapi-x86_64-pc-windows-gnu"
803 | version = "0.4.0"
804 | source = "registry+https://github.com/rust-lang/crates.io-index"
805 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
806 |
807 | [[package]]
808 | name = "windows"
809 | version = "0.57.0"
810 | source = "registry+https://github.com/rust-lang/crates.io-index"
811 | checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
812 | dependencies = [
813 | "windows-core 0.57.0",
814 | "windows-targets",
815 | ]
816 |
817 | [[package]]
818 | name = "windows-core"
819 | version = "0.52.0"
820 | source = "registry+https://github.com/rust-lang/crates.io-index"
821 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
822 | dependencies = [
823 | "windows-targets",
824 | ]
825 |
826 | [[package]]
827 | name = "windows-core"
828 | version = "0.57.0"
829 | source = "registry+https://github.com/rust-lang/crates.io-index"
830 | checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
831 | dependencies = [
832 | "windows-implement",
833 | "windows-interface",
834 | "windows-result",
835 | "windows-targets",
836 | ]
837 |
838 | [[package]]
839 | name = "windows-implement"
840 | version = "0.57.0"
841 | source = "registry+https://github.com/rust-lang/crates.io-index"
842 | checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
843 | dependencies = [
844 | "proc-macro2",
845 | "quote",
846 | "syn",
847 | ]
848 |
849 | [[package]]
850 | name = "windows-interface"
851 | version = "0.57.0"
852 | source = "registry+https://github.com/rust-lang/crates.io-index"
853 | checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
854 | dependencies = [
855 | "proc-macro2",
856 | "quote",
857 | "syn",
858 | ]
859 |
860 | [[package]]
861 | name = "windows-result"
862 | version = "0.1.2"
863 | source = "registry+https://github.com/rust-lang/crates.io-index"
864 | checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
865 | dependencies = [
866 | "windows-targets",
867 | ]
868 |
869 | [[package]]
870 | name = "windows-sys"
871 | version = "0.52.0"
872 | source = "registry+https://github.com/rust-lang/crates.io-index"
873 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
874 | dependencies = [
875 | "windows-targets",
876 | ]
877 |
878 | [[package]]
879 | name = "windows-targets"
880 | version = "0.52.6"
881 | source = "registry+https://github.com/rust-lang/crates.io-index"
882 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
883 | dependencies = [
884 | "windows_aarch64_gnullvm",
885 | "windows_aarch64_msvc",
886 | "windows_i686_gnu",
887 | "windows_i686_gnullvm",
888 | "windows_i686_msvc",
889 | "windows_x86_64_gnu",
890 | "windows_x86_64_gnullvm",
891 | "windows_x86_64_msvc",
892 | ]
893 |
894 | [[package]]
895 | name = "windows_aarch64_gnullvm"
896 | version = "0.52.6"
897 | source = "registry+https://github.com/rust-lang/crates.io-index"
898 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
899 |
900 | [[package]]
901 | name = "windows_aarch64_msvc"
902 | version = "0.52.6"
903 | source = "registry+https://github.com/rust-lang/crates.io-index"
904 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
905 |
906 | [[package]]
907 | name = "windows_i686_gnu"
908 | version = "0.52.6"
909 | source = "registry+https://github.com/rust-lang/crates.io-index"
910 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
911 |
912 | [[package]]
913 | name = "windows_i686_gnullvm"
914 | version = "0.52.6"
915 | source = "registry+https://github.com/rust-lang/crates.io-index"
916 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
917 |
918 | [[package]]
919 | name = "windows_i686_msvc"
920 | version = "0.52.6"
921 | source = "registry+https://github.com/rust-lang/crates.io-index"
922 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
923 |
924 | [[package]]
925 | name = "windows_x86_64_gnu"
926 | version = "0.52.6"
927 | source = "registry+https://github.com/rust-lang/crates.io-index"
928 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
929 |
930 | [[package]]
931 | name = "windows_x86_64_gnullvm"
932 | version = "0.52.6"
933 | source = "registry+https://github.com/rust-lang/crates.io-index"
934 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
935 |
936 | [[package]]
937 | name = "windows_x86_64_msvc"
938 | version = "0.52.6"
939 | source = "registry+https://github.com/rust-lang/crates.io-index"
940 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
941 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2 | # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3 | # in the ext/ directory.
4 |
5 | [workspace]
6 | members = ["./ext/sdb", "sdb-shim"]
7 | resolver = "2"
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM yfractal0/sdb
2 |
3 | COPY ./ /sdb
4 | WORKDIR /sdb
5 | RUN --mount=type=ssh /bin/bash -c "source /etc/profile.d/rbenv.sh && bundle install"
6 | RUN --mount=type=ssh /bin/bash -c "source /etc/profile.d/rbenv.sh && bundle exec rake compile"
7 |
8 | CMD ["bash"]
9 |
--------------------------------------------------------------------------------
/Dockerfile.base:
--------------------------------------------------------------------------------
1 | # Download and build headers manually for Docker Desktop, as the required kernel headers may not be available.
2 | FROM ubuntu:22.04 AS header-builder
3 |
4 | COPY ./build/build_kernel_header.sh /usr/local/bin/build_kernel_header
5 | RUN chmod +x /usr/local/bin/build_kernel_header
6 |
7 | RUN apt-get update -y
8 | RUN apt-get install -y wget
9 | # for building headers
10 | RUN apt-get install -y make build-essential flex bison bc
11 |
12 | RUN /usr/local/bin/build_kernel_header
13 |
14 | FROM ubuntu:22.04
15 |
16 | COPY --from=header-builder /linux-headers /linux-headers
17 | RUN mkdir -p /lib/modules/$(uname -r)
18 | RUN ln -s /linux-headers /lib/modules/$(uname -r)/build
19 |
20 | RUN apt-get update -y
21 | # bcc tools are installed in /usr/share/bcc/tools, for example /usr/sbin/opensnoop-bpfcc
22 | RUN apt-get install -y bpfcc-tools
23 |
24 | RUN apt-get install -y curl
25 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain 1.78.0
26 | ENV PATH="/root/.cargo/bin:${PATH}"
27 |
28 | RUN apt-get install -y git
29 | RUN apt-get install -y \
30 | build-essential \
31 | zlib1g-dev \
32 | libssl-dev \
33 | libreadline-dev \
34 | libyaml-dev \
35 | libxml2-dev \
36 | libxslt-dev \
37 | libclang-dev
38 | RUN git clone https://github.com/sstephenson/rbenv.git /root/.rbenv
39 | RUN git clone https://github.com/sstephenson/ruby-build.git /root/.rbenv/plugins/ruby-build
40 | RUN /root/.rbenv/plugins/ruby-build/install.sh
41 | ENV PATH /root/.rbenv/bin:$PATH
42 | RUN echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh
43 | RUN chmod +x /etc/profile.d/rbenv.sh
44 | RUN echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
45 | RUN echo 'eval "$(rbenv init -)"' >> ~/.bashrc
46 | ENV RBENV_ROOT /root/.rbenv
47 | RUN rbenv install 3.1.5 && rbenv global 3.1.5
48 | RUN /bin/bash -c "source /etc/profile.d/rbenv.sh && gem install bundler"
49 |
50 | RUN apt-get install -y vim gdb
51 |
52 | CMD ["bash"]
53 |
--------------------------------------------------------------------------------
/Dockerfile.rails_demo:
--------------------------------------------------------------------------------
1 | FROM yfractal0/sdb
2 |
3 | COPY ./ /sdb
4 |
5 | RUN --mount=type=ssh /bin/bash -c "cd /sdb && source /etc/profile.d/rbenv.sh && bundle install"
6 | RUN --mount=type=ssh /bin/bash -c "cd /sdb && source /etc/profile.d/rbenv.sh && bundle exec rake compile"
7 |
8 | RUN apt-get install -y unzip
9 | RUN wget https://github.com/yfractal/rails_api/archive/refs/heads/main.zip
10 | RUN unzip main.zip
11 |
12 | WORKDIR /rails_api-main
13 | RUN --mount=type=ssh /bin/bash -c "source /etc/profile.d/rbenv.sh && bundle install"
14 | RUN --mount=type=ssh /bin/bash -c "source /etc/profile.d/rbenv.sh && rake db:setup"
15 |
16 | CMD ["bash", "-c", "source /etc/profile.d/rbenv.sh && ruby /sdb/scripts/rails_demo.rb"]
17 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in sdb.gemspec
6 | gemspec
7 |
8 | gem "rake", "~> 13.0"
9 |
10 | gem "rake-compiler"
11 | gem "rb_sys", "~> 0.9.63"
12 |
13 | gem "rspec", "~> 3.0"
14 |
15 | gem 'cpu_time'
16 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | sdb (0.1.0)
5 | cpu_time
6 | puma (>= 6.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | byebug (11.1.3)
12 | cpu_time (0.1.0)
13 | diff-lcs (1.5.1)
14 | nio4r (2.7.4)
15 | puma (6.6.0)
16 | nio4r (~> 2.0)
17 | rake (13.2.1)
18 | rake-compiler (1.2.7)
19 | rake
20 | rb_sys (0.9.102)
21 | rspec (3.13.0)
22 | rspec-core (~> 3.13.0)
23 | rspec-expectations (~> 3.13.0)
24 | rspec-mocks (~> 3.13.0)
25 | rspec-core (3.13.0)
26 | rspec-support (~> 3.13.0)
27 | rspec-expectations (3.13.2)
28 | diff-lcs (>= 1.2.0, < 2.0)
29 | rspec-support (~> 3.13.0)
30 | rspec-mocks (3.13.1)
31 | diff-lcs (>= 1.2.0, < 2.0)
32 | rspec-support (~> 3.13.0)
33 | rspec-support (3.13.1)
34 |
35 | PLATFORMS
36 | arm64-darwin-23
37 | ruby
38 |
39 | DEPENDENCIES
40 | byebug
41 | cpu_time
42 | rake (~> 13.0)
43 | rake-compiler
44 | rb_sys (~> 0.9.63)
45 | rspec (~> 3.0)
46 | sdb!
47 |
48 | BUNDLED WITH
49 | 2.5.16
50 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Mike Yang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
SDB: High-accuracy stack profiler for Ruby
2 |
3 | # Introduction
4 | SDB provides high-accuracy stack profiling for Ruby applications. It can profile Ruby at **microsecond (0.000001 second)** intervals and offers event tagging for requests.
5 |
6 | # Why Do We Need Another Stack Profiler?
7 | 1. Minimal Impact: SDB does not affect the target application's performance.
8 | 2. Event Tagging: SDB supports event tagging, making it easier to trace and identify slow functions.
9 | 3. High Accuracy: SDB offers precision down to microseconds.
10 |
11 | Instrumentation libraries like `opentelemetry-ruby` generate data during application runtime, and some stack profilers block application threads. These approaches can introduce delays in your application.
12 |
13 | SDB, inspired by [LDB](https://www.usenix.org/conference/nsdi24/presentation/cho#:~:text=LDB%20observes%20the%20latency%20of,costs%20away%20from%20program%20threads.), operates by pulling stack traces on a separate thread, which minimizes the impact on our application.
14 |
15 | Event tagging, such as adding trace_id for Puma requests, makes it easier to identify slow functions.
16 |
17 | Moreover, SDB can precisely identify delays, down to a single microsecond.
18 |
19 | # How it Works
20 |
21 |
22 | The stack scanner scans the Ruby stacks without the GVL as reading an Iseq address is atomic.
23 | The symbolizer translates Iseq into function name and path and it "caches" the translation result for avoiding repeat work to achieve better performance. As it "caches" the translation result, it marks the Iseq address at each beginning of GC for avoiding incorrect results -- an Iseq has been reclaimed and the address is used by a new Iseq.
24 |
25 | # Usage Example
26 | 
27 |
28 | The image above is generated by [sdb-analyzer](https://github.com/yfractal/sdb-analyzer). It represents a simple Ruby API application rendering an empty body.
29 |
30 | The image clearly shows which methods are used and their latency, helping us understand the application's behavior and identify potential latency issues, even without prior background knowledge.
31 |
32 | # NOTICE
33 | SDB is still in **the experimental stage**. Rather than focusing on ease of use and stability, I am exploring additional use cases, such as detecting concurrency issues or delays caused by GVL.
34 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 |
6 | RSpec::Core::RakeTask.new(:spec)
7 |
8 | require "rb_sys/extensiontask"
9 |
10 | task build: :compile
11 |
12 | GEMSPEC = Gem::Specification.load("sdb.gemspec")
13 |
14 | RbSys::ExtensionTask.new("sdb", GEMSPEC) do |ext|
15 | ext.lib_dir = "lib/sdb"
16 | end
17 |
18 | task default: %i[compile spec]
19 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "sdb"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | require "irb"
11 | IRB.start(__FILE__)
12 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/build/build_kernel_header.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Configure the headers
4 | echo 'Configuring headers...'
5 | major_version=$(uname -r | sed -E 's/^([0-9]+\.[0-9]+).*/\1/')
6 | wget https://github.com/torvalds/linux/archive/refs/tags/v${major_version}.tar.gz
7 |
8 | mkdir linux-headers
9 |
10 | tar --strip-components=1 -xzf v${major_version}.tar.gz -C linux-headers
11 |
12 | cd linux-headers
13 |
14 | # Create a ./.config file by using the default
15 | # symbol values from either arch/$ARCH/defconfig
16 | # or arch/$ARCH/configs/${PLATFORM}_defconfig,
17 | # depending on the architecture.
18 | make defconfig
19 |
20 | # Create module symlinks
21 | echo 'CONFIG_BPF=y' >> .config
22 | echo 'CONFIG_BPF_SYSCALL=y' >> .config
23 | echo 'CONFIG_BPF_JIT=y' >> .config
24 | echo 'CONFIG_HAVE_EBPF_JIT=y' >> .config
25 | echo 'CONFIG_BPF_EVENTS=y' >> .config
26 | echo 'CONFIG_FTRACE_SYSCALLS=y' >> .config
27 | echo 'CONFIG_KALLSYMS_ALL=y' >> .config
28 |
29 | # prepare headers
30 | echo 'Preparing headers...'
31 | make prepare
32 |
--------------------------------------------------------------------------------
/document/safety_argument.md:
--------------------------------------------------------------------------------
1 | # Safety Argument
2 | ## Why there are so many `unsafe` in SDB
3 | SDB has 2 design goals:
4 | 1. Scanning the Ruby stack without GVL
5 | 2. Building a non-blocking Ruby stack profiler
6 |
7 | A stack profiler is a tool for observability that should not impact applications. However, most Ruby stack profilers need GVL when scanning the Ruby stack, which reduces application performance. For example, a stack profiler may increase latency by 10% for a Rails request at a 1ms scanning interval. Many tools increase the scanning interval to reduce this impact; for instance, the Datadog Ruby Agent uses a 10ms scanning interval, which results in less accurate profiling results and can't detect certain performance issues.
8 |
9 | Thus, I believe the first design goal is necessary for a Ruby stack profiler.
10 |
11 | Achieving the first design goal could solve 90% or even 99% of performance issues. However, if we do not use synchronization primitives carefully, it can still block Ruby applications in certain situations. Compared to the first design goal, the second one is less impactful but more challenging and interesting.
12 |
13 | Because of these design goals, the Rust safety mechanism is not enough. Additionally, it seems that there is no good answer on how to use safe code when using Rust in other systems. For example, in Rust-for-Linux, comments are needed to reason about the safety of unsafe blocks[1]. Magnus[2] also depends on many `unsafe` code blocks and restricts usage, such as not allowing passing Ruby objects to the heap in Rust.
14 |
15 | ## Scanning the Ruby Stack without the GVL
16 | Please check this article: https://github.com/yfractal/blog/blob/master/blog/2025-01-15-non-blocking-stack-profiler.md
17 |
18 | ## The Trace-id Hash Table
19 |
20 | The trace-id hash table maintains per-thread trace-ids for SDB. Ruby threads update this hash table, and the SDB scanner thread reads the values. This means the hash table is accessed concurrently, but to fulfill the second design goal, it doesn't use an additional lock.
21 |
22 | To use a hash table safely, we need to consider:
23 |
24 | 1. Before resizing a hash table, we should have exclusive access.
25 | 2. When querying a hash table, we should see the most recent write.
26 |
27 | When creating a new Ruby thread, a mutex (`STACK_SCANNER`) is acquired for updating the threads list. This mutex is used for guarantee exclusive access when updating the threads list, while this lock is held, the scanner is blocked too. During this time, a dummy trace-id is inserted for the thread into the table. Similarly, when a Ruby thread is being reclaimed, the lock is acquired, and the thread is deleted from the table. Hash resizing can only happen when a new key is added or a key is deleted. Since the lock is held during these operations, the scanner thread does not read the table, fulfilling the first consideration.
28 |
29 | For the second consideration, SDB uses atomic variables for the hash table's values with memory ordering (release order when updating and acquire order when reading).
30 |
31 | A lock (whether a mutex or spinlock) requires updating an atomic value at least twice (acquiring the lock and releasing the lock). Making the value an atomic variable only requires one atomic update, so it could be more efficient. From an engineer's point of view, such optimization may not be worth it as it introduces complexity with little benefit. However, SDB works as an experimental project, and such an implementation is interesting and challenging.
32 |
33 | ## References
34 | 1. An Empirical Study of Rust-for-Linux: The Success, Dissatisfaction, and Compromise
35 | 2. https://github.com/matsadler/magnus?tab=readme-ov-file#safety
36 |
--------------------------------------------------------------------------------
/examples/aggregrate.rb:
--------------------------------------------------------------------------------
1 | require 'sdb'
2 |
3 | def f1
4 | rand
5 | end
6 |
7 | def f2
8 | rand
9 | end
10 |
11 | def f3
12 | rand
13 | end
14 |
15 | def f4
16 | rand
17 | end
18 |
19 | def ffffff
20 | f1
21 | f2
22 | if rand < 0.2
23 | f3
24 | else
25 | f4
26 | end
27 | end
28 |
29 | 2.times do
30 | Thread.new do
31 | i = 0
32 | loop do
33 | if i == 10000
34 | i = 0
35 | puts "looping #{Thread.current}"
36 |
37 | sleep 2
38 | end
39 | i += 1
40 |
41 | ffffff
42 | end
43 | end
44 | end
45 |
46 | sleep 1
47 |
48 | # Sdb.busy_pull(Thread.list)
49 | Sdb.pull(Thread.list, 0.1)
50 |
--------------------------------------------------------------------------------
/examples/benchmark.rb:
--------------------------------------------------------------------------------
1 | require 'sdb'
2 |
3 | def test(stacks_depth, n)
4 | if stacks_depth > 0
5 | test(stacks_depth - 1, n)
6 | else
7 | t0 = Time.now
8 | while n > 0
9 | n -= 1
10 | end
11 | t1 = Time.now
12 | puts "Takes = #{t1 - t0}"
13 | end
14 | end
15 |
16 | # sleep_interval = 0.001
17 | # sleep_interval = 0.0_001
18 | # sleep_interval = 0.00_001
19 | # sleep_interval = 0.000_001
20 | sleep_interval = 0.0000_0_01
21 | Sdb.scan_all_threads(sleep_interval)
22 | test(150, 500_000_000)
23 |
24 |
25 | # base line
26 | # Takes = 7.874596155
27 | #
28 | # 0.001 ns
29 | # sleep interval 1000 ns
30 | #
31 | # Takes = 7.872404624
32 | # sleep interval 10 ns
33 | # Takes = 7.884911666
34 | #
35 | # sleep interval 1 ns
36 | # Takes = 7.905451317
37 | #
38 | # sleep interval 0 ns
39 | # Takes = 7.889088806
40 |
--------------------------------------------------------------------------------
/examples/demo.rb:
--------------------------------------------------------------------------------
1 | require 'sdb'
2 |
3 | def a
4 | b
5 | end
6 |
7 | def b
8 | c
9 | end
10 |
11 | def c
12 | sleep 0.1
13 | end
14 | def f1
15 | rand
16 | end
17 |
18 | def f2
19 | "abc" * rand(100)
20 | end
21 |
22 | def f3
23 | rand
24 | end
25 |
26 | def f4
27 | rand
28 | end
29 |
30 | def ffffff
31 | f1
32 | f2
33 | a
34 | if rand < 0.2
35 | f3
36 | else
37 | f4
38 | end
39 | end
40 |
41 | 2.times do
42 | Thread.new do
43 | i = 0
44 | loop do
45 | if i == 10000
46 | i = 0
47 | puts "looping #{Thread.current}"
48 |
49 | sleep 0.1
50 | end
51 | i += 1
52 |
53 | ffffff
54 | end
55 | end
56 | end
57 |
58 | sleep 1
59 |
60 | Sdb.scan_all_threads(0.1)
61 | sleep 30
62 |
--------------------------------------------------------------------------------
/ext/sdb/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sdb"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["Mike Yang "]
6 | license = "MIT"
7 | publish = false
8 |
9 | [lib]
10 | crate-type = ["cdylib"]
11 |
12 | [dependencies]
13 | chrono = "0.4.38"
14 | fast_log = "1.7.3"
15 | lazy_static = "1.5.0"
16 | libc = "0.2.155"
17 | log = "0.4.22"
18 | rb-sys = { version = "0.9.99", features = ["stable-api", "stable-api-compiled-fallback"]}
19 | rbspy-ruby-structs = "0.34.1"
20 | serde = { version = "1.0.205", features = ["derive"] }
21 | serde_json = "1.0.122"
22 | spin = "0.9.8"
23 | sysinfo = "0.33.1"
24 |
--------------------------------------------------------------------------------
/ext/sdb/extconf.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "mkmf"
4 | require "rb_sys/mkmf"
5 |
6 | create_rust_makefile("sdb/sdb")
7 |
--------------------------------------------------------------------------------
/ext/sdb/src/gvl.rs:
--------------------------------------------------------------------------------
1 | use libc::{pthread_self, pthread_t};
2 | use rb_sys::{rb_ll2inum, RTypedData, VALUE};
3 | use rbspy_ruby_structs::ruby_3_1_5::{rb_global_vm_lock_t, rb_thread_t};
4 |
5 | pub(crate) unsafe extern "C" fn rb_log_gvl_addr(_module: VALUE, thread_val: VALUE) -> VALUE {
6 | // todo: handle logger initialization
7 | let thread_ptr: *mut RTypedData = thread_val as *mut RTypedData;
8 | let rb_thread_ptr = (*thread_ptr).data as *mut rb_thread_t;
9 |
10 | // access gvl_addr through offset directly
11 | let gvl_addr = (*rb_thread_ptr).ractor as u64 + 344;
12 | let gvl_ref = gvl_addr as *mut rb_global_vm_lock_t;
13 | let lock_addr = &((*gvl_ref).lock) as *const _ as u64;
14 | let tid: pthread_t = pthread_self();
15 |
16 | log::info!(
17 | "[lock] thread_id={}, rb_thread_addr={}, gvl_mutex_addr={}",
18 | tid,
19 | rb_thread_ptr as u64,
20 | lock_addr
21 | );
22 |
23 | rb_ll2inum(lock_addr as i64) as VALUE
24 | }
25 |
--------------------------------------------------------------------------------
/ext/sdb/src/helpers.rs:
--------------------------------------------------------------------------------
1 | use libc::{c_char, c_int, c_long, c_void};
2 | use rb_sys::{rb_funcallv, rb_intern2, rb_num2long, Qnil, ID, VALUE};
3 |
4 | #[inline]
5 | pub(crate) fn internal_id(string: &str) -> ID {
6 | let str = string.as_ptr() as *const c_char;
7 | let len = string.len() as c_long;
8 |
9 | unsafe { rb_intern2(str, len) }
10 | }
11 |
12 | #[inline]
13 | pub(crate) fn call_method(receiver: VALUE, method: &str, argc: c_int, argv: &[VALUE]) -> VALUE {
14 | let id = internal_id(method);
15 | unsafe { rb_funcallv(receiver, id, argc, argv as *const [VALUE] as *const VALUE) }
16 | }
17 |
18 | pub(crate) unsafe extern "C" fn rb_first_lineno_from_iseq_addr(
19 | _module: VALUE,
20 | iseq_addr: VALUE,
21 | ) -> VALUE {
22 | let iseq_addr = rb_num2long(iseq_addr) as *const c_void as u64;
23 |
24 | if iseq_addr == 0 {
25 | return Qnil as VALUE;
26 | }
27 |
28 | crate::stack_scanner::RUBY_API.get_first_lineno(iseq_addr)
29 | }
30 |
31 | pub(crate) unsafe extern "C" fn rb_label_from_iseq_addr(_module: VALUE, iseq_addr: VALUE) -> VALUE {
32 | let iseq_addr = rb_num2long(iseq_addr) as *const c_void as u64;
33 |
34 | if iseq_addr == 0 {
35 | return Qnil as VALUE;
36 | }
37 |
38 | crate::stack_scanner::RUBY_API.get_label(iseq_addr)
39 | }
40 |
41 | pub(crate) unsafe extern "C" fn rb_base_label_from_iseq_addr(
42 | _module: VALUE,
43 | iseq_addr: VALUE,
44 | ) -> VALUE {
45 | let iseq_addr = rb_num2long(iseq_addr) as *const c_void as u64;
46 |
47 | if iseq_addr == 0 {
48 | return Qnil as VALUE;
49 | }
50 |
51 | crate::stack_scanner::RUBY_API.get_base_label(iseq_addr)
52 | }
53 |
--------------------------------------------------------------------------------
/ext/sdb/src/iseq_logger.rs:
--------------------------------------------------------------------------------
1 | const ISEQS_BUFFER_SIZE: usize = 1_000_000;
2 |
3 | pub struct IseqLogger {
4 | buffer: [u64; ISEQS_BUFFER_SIZE],
5 | buffer_size: usize,
6 | buffer_index: usize,
7 | }
8 |
9 | impl IseqLogger {
10 | pub fn new() -> Self {
11 | IseqLogger {
12 | buffer: [0; ISEQS_BUFFER_SIZE],
13 | buffer_size: ISEQS_BUFFER_SIZE,
14 | buffer_index: 0,
15 | }
16 | }
17 |
18 | #[inline]
19 | pub fn push(&mut self, item: u64) {
20 | if self.buffer_index < self.buffer_size {
21 | self.buffer[self.buffer_index] = item;
22 | self.buffer_index += 1;
23 | } else {
24 | log::info!("[{}][stack_frames]{:?}", std::process::id(), self.buffer);
25 | self.buffer_index = 0;
26 | }
27 | }
28 |
29 | #[inline]
30 | pub fn push_seperator(&mut self) {
31 | self.push(u64::MAX);
32 | self.push(u64::MAX);
33 | }
34 |
35 | #[inline]
36 | pub fn flush(&mut self) {
37 | log::info!(
38 | "[{}][stack_frames]{:?}",
39 | std::process::id(),
40 | &self.buffer[..self.buffer_index]
41 | );
42 | self.buffer_index = 0;
43 | log::logger().flush();
44 | }
45 |
46 | #[inline]
47 | pub fn log(&mut self, str: &str) {
48 | log::info!("[{}]{}", std::process::id(), str);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ext/sdb/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod gvl;
2 | mod helpers;
3 | mod iseq_logger;
4 | mod logger;
5 | mod ruby_version;
6 | mod stack_scanner;
7 | mod tester;
8 | mod trace_id;
9 |
10 | use libc::c_char;
11 | use rb_sys::{
12 | rb_define_module, rb_define_singleton_method, rb_tracepoint_enable, rb_tracepoint_new, Qnil,
13 | VALUE,
14 | };
15 |
16 | use gvl::*;
17 | use helpers::*;
18 | use logger::*;
19 | use stack_scanner::*;
20 | use std::os::raw::c_void;
21 | use tester::*;
22 | use trace_id::*;
23 |
24 | use lazy_static::lazy_static;
25 |
26 | lazy_static! {
27 | static ref SDB_MODULE: u64 =
28 | unsafe { rb_define_module("Sdb\0".as_ptr() as *const c_char) as u64 };
29 | }
30 |
31 | pub(crate) unsafe extern "C" fn rb_init_logger(_module: VALUE) -> VALUE {
32 | init_logger();
33 | return Qnil as VALUE;
34 | }
35 |
36 | extern "C" fn gc_enter_callback(_trace_point: VALUE, _data: *mut c_void) {
37 | // acquire stack_scanner lock for blocking the scanning
38 | let mut stack_scanner = STACK_SCANNER.lock();
39 | stack_scanner.pause();
40 | stack_scanner.consume_iseq_buffer();
41 | stack_scanner.mark_iseqs();
42 |
43 | let (lock, _) = &*START_TO_PULL_COND_VAR;
44 | let mut start = lock.lock().unwrap();
45 | *start = false;
46 |
47 | // Ruby uses GVL, the drop order is not matter actually.
48 | // But drop the stack_scanner later can guarantee the scanner go out from the looping_helper and then sees the condvar.
49 | drop(start);
50 | drop(stack_scanner);
51 | }
52 |
53 | unsafe extern "C" fn gc_exist_callback(_trace_point: VALUE, _data: *mut c_void) {
54 | let mut stack_scanner = STACK_SCANNER.lock();
55 |
56 | if stack_scanner.is_paused() {
57 | let (lock, cvar) = &*START_TO_PULL_COND_VAR;
58 | let mut start = lock.lock().unwrap();
59 | stack_scanner.resume();
60 | *start = true;
61 |
62 | // triggers the scanner thread, here, we still hold the stack_scanner lock,
63 | // after the stack_scanner lock is dropped, the scanner starts to scan,
64 | // or it could pin for a very short period of time.
65 | cvar.notify_one();
66 | }
67 | }
68 |
69 | pub(crate) unsafe extern "C" fn setup_gc_hooks(_module: VALUE) -> VALUE {
70 | unsafe {
71 | let tp = rb_tracepoint_new(
72 | 0,
73 | rb_sys::RUBY_INTERNAL_EVENT_GC_ENTER,
74 | Some(gc_enter_callback),
75 | std::ptr::null_mut(),
76 | );
77 | rb_tracepoint_enable(tp);
78 |
79 | let tp_exist = rb_tracepoint_new(
80 | 0,
81 | rb_sys::RUBY_INTERNAL_EVENT_GC_EXIT,
82 | Some(gc_exist_callback),
83 | std::ptr::null_mut(),
84 | );
85 | rb_tracepoint_enable(tp_exist);
86 | }
87 |
88 | return Qnil as VALUE;
89 | }
90 |
91 | #[allow(non_snake_case)]
92 | #[no_mangle]
93 | extern "C" fn Init_sdb() {
94 | macro_rules! define_ruby_method {
95 | ($module:expr, $name:expr, $callback:expr, 0) => {
96 | let transmuted_callback = std::mem::transmute::<
97 | unsafe extern "C" fn(VALUE) -> VALUE,
98 | unsafe extern "C" fn() -> VALUE,
99 | >($callback);
100 | rb_define_singleton_method(
101 | $module,
102 | format!("{}\0", $name).as_ptr() as _,
103 | Some(transmuted_callback),
104 | 0,
105 | );
106 | };
107 | ($module:expr, $name:expr, $callback:expr, 1) => {
108 | let transmuted_callback = std::mem::transmute::<
109 | unsafe extern "C" fn(VALUE, VALUE) -> VALUE,
110 | unsafe extern "C" fn() -> VALUE,
111 | >($callback);
112 | rb_define_singleton_method(
113 | $module,
114 | format!("{}\0", $name).as_ptr() as _,
115 | Some(transmuted_callback),
116 | 1,
117 | );
118 | };
119 | ($module:expr, $name:expr, $callback:expr, 2) => {
120 | let transmuted_callback = std::mem::transmute::<
121 | unsafe extern "C" fn(VALUE, VALUE, VALUE) -> VALUE,
122 | unsafe extern "C" fn() -> VALUE,
123 | >($callback);
124 | rb_define_singleton_method(
125 | $module,
126 | format!("{}\0", $name).as_ptr() as _,
127 | Some(transmuted_callback),
128 | 2,
129 | );
130 | };
131 | }
132 |
133 | unsafe {
134 | let module = rb_define_module("Sdb\0".as_ptr() as *const c_char);
135 |
136 | define_ruby_method!(module, "pull", rb_pull, 1);
137 | define_ruby_method!(module, "set_trace_id", rb_set_trace_id, 2);
138 | define_ruby_method!(module, "log_gvl_addr_for_thread", rb_log_gvl_addr, 1);
139 | define_ruby_method!(
140 | module,
141 | "on_stack_func_addresses",
142 | rb_get_on_stack_func_addresses,
143 | 1
144 | );
145 | define_ruby_method!(
146 | module,
147 | "first_lineno_from_iseq_addr",
148 | rb_first_lineno_from_iseq_addr,
149 | 1
150 | );
151 | define_ruby_method!(module, "label_from_iseq_addr", rb_label_from_iseq_addr, 1);
152 | define_ruby_method!(
153 | module,
154 | "base_label_from_iseq_addr",
155 | rb_base_label_from_iseq_addr,
156 | 1
157 | );
158 | define_ruby_method!(module, "init_logger", rb_init_logger, 0);
159 | define_ruby_method!(
160 | module,
161 | "log_uptime_and_clock_time",
162 | rb_log_uptime_and_clock_time,
163 | 0
164 | );
165 | define_ruby_method!(
166 | module,
167 | "update_threads_to_scan",
168 | rb_update_threads_to_scan,
169 | 1
170 | );
171 | define_ruby_method!(module, "stop_scanner", rb_stop_scanner, 0);
172 | define_ruby_method!(module, "setup_gc_hooks", setup_gc_hooks, 0);
173 |
174 | let sdb_tester = rb_define_module("SdbTester\0".as_ptr() as *const c_char);
175 | define_ruby_method!(sdb_tester, "ec_from_thread", rb_get_ec_from_thread, 1);
176 | define_ruby_method!(sdb_tester, "iseqs_from_ec", rb_get_iseqs, 1);
177 | define_ruby_method!(sdb_tester, "is_iseq_imemo", rb_is_iseq_imemo, 1);
178 | define_ruby_method!(sdb_tester, "iseq_info", rb_get_iseq_info, 1);
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/ext/sdb/src/logger.rs:
--------------------------------------------------------------------------------
1 | use fast_log::config::Config;
2 | use fast_log::Logger;
3 |
4 | const FAST_LOG_CHAN_LEN: usize = 100_000;
5 |
6 | pub(crate) fn init_logger() -> &'static Logger {
7 | // TODO: check why unwrap may panic in rspec
8 | // reproduce: RUST_BACKTRACE=1 bundle exec rspec spec/sdb_spec.rb
9 | fast_log::init(
10 | Config::new()
11 | .file("sdb.log")
12 | .chan_len(Some(FAST_LOG_CHAN_LEN)),
13 | )
14 | .unwrap()
15 | }
16 |
--------------------------------------------------------------------------------
/ext/sdb/src/ruby_version.rs:
--------------------------------------------------------------------------------
1 | use libc::c_void;
2 | use rb_sys::VALUE;
3 | use std::ffi::CStr;
4 | use std::os::raw::c_char;
5 |
6 | const MAX_STR_LENGTH: usize = 127;
7 | const RSTRING_HEAP_FLAGS: usize = 1 << 13;
8 |
9 | macro_rules! impl_ruby_str_to_rust_str {
10 | ($rstring_type:path) => {
11 | #[inline]
12 | unsafe fn ruby_str_to_rust_str(&self, ruby_str: VALUE) -> Option {
13 | use $rstring_type as RString;
14 |
15 | let str_ptr = ruby_str as *const RString;
16 |
17 | if ruby_str == 0 {
18 | return None;
19 | }
20 |
21 | if str_ptr.is_null() {
22 | return None;
23 | }
24 |
25 | let str_ref = &*str_ptr;
26 | let flags = str_ref.basic.flags;
27 |
28 | if flags & RSTRING_HEAP_FLAGS != 0 {
29 | // For heap strings, we need to use strlen to find the end of the string
30 | // because the length is not readily available in rbspy structs
31 | let ptr = str_ref.as_.heap.ptr;
32 |
33 | if ptr.is_null() {
34 | None
35 | } else {
36 | // Use strlen to find the length of the null-terminated string
37 | let len = rb_sys::RSTRING_LEN(ruby_str) as usize;
38 |
39 | if len == 0 {
40 | Some(String::new())
41 | } else {
42 | let bytes = std::slice::from_raw_parts(ptr as *const u8, len);
43 | Some(String::from_utf8_lossy(bytes).into_owned())
44 | }
45 | }
46 | } else {
47 | // Embedded string
48 | let ary = str_ref.as_.embed.ary.as_ptr();
49 | let mut len = 0;
50 |
51 | for i in 0..MAX_STR_LENGTH {
52 | if *ary.add(i) == 0 {
53 | break;
54 | }
55 | len += 1;
56 | }
57 |
58 | let bytes = std::slice::from_raw_parts(ary as *const u8, len);
59 | Some(String::from_utf8_lossy(bytes).into_owned())
60 | }
61 | }
62 | };
63 | }
64 |
65 | macro_rules! impl_iseq_functions {
66 | ($iseq_struct:path) => {
67 | #[inline]
68 | unsafe fn get_iseq_info(&self, iseq_addr: u64) -> (Option, Option) {
69 | use $iseq_struct as rb_iseq_struct;
70 | let iseq = &*(iseq_addr as *const rb_iseq_struct);
71 | let body = &*iseq.body;
72 |
73 | let label = body.location.label as VALUE;
74 | let label_str = self.ruby_str_to_rust_str(label);
75 |
76 | let path = body.location.pathobj as VALUE;
77 | let path_str = self.extract_path_string(path);
78 |
79 | (label_str, path_str)
80 | }
81 |
82 | #[inline]
83 | unsafe fn get_first_lineno(&self, iseq_addr: u64) -> VALUE {
84 | use $iseq_struct as rb_iseq_struct;
85 | let iseq = &*(iseq_addr as *const rb_iseq_struct);
86 | let body = &*iseq.body;
87 | body.location.first_lineno as VALUE
88 | }
89 |
90 | #[inline]
91 | unsafe fn get_label(&self, iseq_addr: u64) -> VALUE {
92 | use $iseq_struct as rb_iseq_struct;
93 | let iseq = &*(iseq_addr as *const rb_iseq_struct);
94 | let body = &*iseq.body;
95 | body.location.label as VALUE
96 | }
97 |
98 | #[inline]
99 | unsafe fn get_base_label(&self, iseq_addr: u64) -> VALUE {
100 | use $iseq_struct as rb_iseq_struct;
101 | let iseq = &*(iseq_addr as *const rb_iseq_struct);
102 | let body = &*iseq.body;
103 | body.location.base_label as VALUE
104 | }
105 |
106 | #[inline]
107 | unsafe fn extract_path_string(&self, path: VALUE) -> Option {
108 | if path == 0 {
109 | return None;
110 | }
111 |
112 | let basic_flags = *(path as *const rb_sys::RBasic);
113 | let obj_type = basic_flags.flags & (rb_sys::RUBY_T_MASK as u64);
114 |
115 | if obj_type == rb_sys::RUBY_T_STRING as u64 {
116 | self.ruby_str_to_rust_str(path)
117 | } else if obj_type == rb_sys::RUBY_T_ARRAY as u64 {
118 | let array_len = rb_sys::RARRAY_LEN(path);
119 | if array_len >= 1 {
120 | let path_val = rb_sys::rb_ary_entry(path, 0);
121 | self.ruby_str_to_rust_str(path_val)
122 | } else {
123 | None
124 | }
125 | } else {
126 | None
127 | }
128 | }
129 |
130 | #[inline]
131 | unsafe fn is_iseq_imemo(&self, iseq_ptr: *const c_void) -> bool {
132 | if iseq_ptr.is_null() {
133 | return false;
134 | }
135 |
136 | use $iseq_struct as rb_iseq_struct;
137 | let iseq = &*(iseq_ptr as *const rb_iseq_struct);
138 | const FL_USHIFT: usize = 12;
139 | const IMEMO_MASK: usize = 0x0F;
140 | const IMEMO_ISEQ: usize = 7;
141 | (iseq.flags >> FL_USHIFT) & IMEMO_MASK == IMEMO_ISEQ
142 | }
143 | };
144 | }
145 |
146 | macro_rules! impl_thread_functions {
147 | ($thread_struct:path) => {
148 | #[inline]
149 | unsafe fn get_ec_from_thread(&self, thread_val: VALUE) -> *mut c_void {
150 | use rb_sys::RTypedData;
151 | use $thread_struct as rb_thread_t;
152 | let thread_ptr: *mut RTypedData = thread_val as *mut RTypedData;
153 | let thread_struct_ptr: *mut rb_thread_t = (*thread_ptr).data as *mut rb_thread_t;
154 | let thread_struct = &*thread_struct_ptr;
155 | thread_struct.ec as *mut c_void
156 | }
157 | };
158 | }
159 |
160 | macro_rules! impl_control_frame_functions {
161 | ($control_frame_struct:path, $execution_context_struct:path) => {
162 | #[inline]
163 | fn get_control_frame_struct_size(&self) -> usize {
164 | use $control_frame_struct as rb_control_frame_struct;
165 | std::mem::size_of::()
166 | }
167 |
168 | #[inline]
169 | unsafe fn iterate_frame_iseqs(&self, ec_val: VALUE, iseq_handler: &mut dyn FnMut(u64)) {
170 | use $execution_context_struct as rb_execution_context_struct;
171 | let ec = *(ec_val as *mut rb_execution_context_struct);
172 | let stack_base = ec.vm_stack.add(ec.vm_stack_size);
173 | let diff = (stack_base as usize) - (ec.cfp as usize);
174 | let len = diff / self.get_control_frame_struct_size();
175 | let frames = std::slice::from_raw_parts(ec.cfp, len);
176 |
177 | for frame in frames {
178 | let iseq = &*frame.iseq;
179 | let iseq_addr = iseq as *const _ as u64;
180 | iseq_handler(iseq_addr);
181 | }
182 | }
183 | };
184 | }
185 |
186 | #[derive(Debug, Clone, Copy, PartialEq)]
187 | pub enum RubyVersion {
188 | // Ruby 3.1.x
189 | Ruby310,
190 | Ruby311,
191 | Ruby312,
192 | Ruby313,
193 | Ruby314,
194 | Ruby315,
195 | Ruby316,
196 | Ruby317,
197 |
198 | // Ruby 3.2.x
199 | Ruby320,
200 | Ruby321,
201 | Ruby322,
202 | Ruby323,
203 | Ruby324,
204 | Ruby325,
205 | Ruby326,
206 | Ruby327,
207 | Ruby328,
208 |
209 | // Ruby 3.3.x
210 | Ruby330,
211 | Ruby331,
212 | Ruby332,
213 | Ruby333,
214 | Ruby334,
215 | Ruby335,
216 | Ruby336,
217 | Ruby337,
218 | Ruby338,
219 |
220 | // Ruby 3.4.x
221 | Ruby340,
222 | Ruby341,
223 | Ruby342,
224 | Ruby343,
225 | Ruby344,
226 | }
227 |
228 | pub trait RubyApiCompat: Send + Sync {
229 | unsafe fn get_iseq_info(&self, iseq_addr: u64) -> (Option, Option);
230 | unsafe fn get_first_lineno(&self, iseq_addr: u64) -> VALUE;
231 | unsafe fn get_label(&self, iseq_addr: u64) -> VALUE;
232 | unsafe fn get_base_label(&self, iseq_addr: u64) -> VALUE;
233 | unsafe fn ruby_str_to_rust_str(&self, ruby_str: VALUE) -> Option;
234 | unsafe fn extract_path_string(&self, path: VALUE) -> Option;
235 | unsafe fn is_iseq_imemo(&self, iseq_ptr: *const c_void) -> bool;
236 | unsafe fn get_ec_from_thread(&self, thread_val: VALUE) -> *mut c_void;
237 | fn get_control_frame_struct_size(&self) -> usize;
238 | unsafe fn iterate_frame_iseqs(&self, ec_val: VALUE, frame_handler: &mut dyn FnMut(u64));
239 | }
240 |
241 | // Macro to reduce duplication for Ruby version implementations
242 | macro_rules! impl_ruby_version {
243 | // Ruby 3.1.x
244 | (Ruby310) => {
245 | impl_ruby_version_with_module!(Ruby310, ruby_3_1_0);
246 | };
247 | (Ruby311) => {
248 | impl_ruby_version_with_module!(Ruby311, ruby_3_1_1);
249 | };
250 | (Ruby312) => {
251 | impl_ruby_version_with_module!(Ruby312, ruby_3_1_2);
252 | };
253 | (Ruby313) => {
254 | impl_ruby_version_with_module!(Ruby313, ruby_3_1_3);
255 | };
256 | (Ruby314) => {
257 | impl_ruby_version_with_module!(Ruby314, ruby_3_1_4);
258 | };
259 | (Ruby315) => {
260 | impl_ruby_version_with_module!(Ruby315, ruby_3_1_5);
261 | };
262 | (Ruby316) => {
263 | impl_ruby_version_with_module!(Ruby316, ruby_3_1_6);
264 | };
265 | (Ruby317) => {
266 | impl_ruby_version_with_module!(Ruby317, ruby_3_1_7);
267 | };
268 |
269 | // Ruby 3.2.x
270 | (Ruby320) => {
271 | impl_ruby_version_with_module!(Ruby320, ruby_3_2_0);
272 | };
273 | (Ruby321) => {
274 | impl_ruby_version_with_module!(Ruby321, ruby_3_2_1);
275 | };
276 | (Ruby322) => {
277 | impl_ruby_version_with_module!(Ruby322, ruby_3_2_2);
278 | };
279 | (Ruby323) => {
280 | impl_ruby_version_with_module!(Ruby323, ruby_3_2_3);
281 | };
282 | (Ruby324) => {
283 | impl_ruby_version_with_module!(Ruby324, ruby_3_2_4);
284 | };
285 | (Ruby325) => {
286 | impl_ruby_version_with_module!(Ruby325, ruby_3_2_5);
287 | };
288 | (Ruby326) => {
289 | impl_ruby_version_with_module!(Ruby326, ruby_3_2_6);
290 | };
291 | (Ruby327) => {
292 | impl_ruby_version_with_module!(Ruby327, ruby_3_2_7);
293 | };
294 | (Ruby328) => {
295 | impl_ruby_version_with_module!(Ruby328, ruby_3_2_8);
296 | };
297 |
298 | // Ruby 3.3.x
299 | (Ruby330) => {
300 | impl_ruby_version_with_module!(Ruby330, ruby_3_3_0);
301 | };
302 | (Ruby331) => {
303 | impl_ruby_version_with_module!(Ruby331, ruby_3_3_1);
304 | };
305 | (Ruby332) => {
306 | impl_ruby_version_with_module!(Ruby332, ruby_3_3_2);
307 | };
308 | (Ruby333) => {
309 | impl_ruby_version_with_module!(Ruby333, ruby_3_3_3);
310 | };
311 | (Ruby334) => {
312 | impl_ruby_version_with_module!(Ruby334, ruby_3_3_4);
313 | };
314 | (Ruby335) => {
315 | impl_ruby_version_with_module!(Ruby335, ruby_3_3_5);
316 | };
317 | (Ruby336) => {
318 | impl_ruby_version_with_module!(Ruby336, ruby_3_3_6);
319 | };
320 | (Ruby337) => {
321 | impl_ruby_version_with_module!(Ruby337, ruby_3_3_7);
322 | };
323 | (Ruby338) => {
324 | impl_ruby_version_with_module!(Ruby338, ruby_3_3_8);
325 | };
326 |
327 | // Ruby 3.4.x
328 | (Ruby340) => {
329 | impl_ruby_version_with_module!(Ruby340, ruby_3_4_0);
330 | };
331 | (Ruby341) => {
332 | impl_ruby_version_with_module!(Ruby341, ruby_3_4_1);
333 | };
334 | (Ruby342) => {
335 | impl_ruby_version_with_module!(Ruby342, ruby_3_4_2);
336 | };
337 | (Ruby343) => {
338 | impl_ruby_version_with_module!(Ruby343, ruby_3_4_3);
339 | };
340 | (Ruby344) => {
341 | impl_ruby_version_with_module!(Ruby344, ruby_3_4_4);
342 | };
343 | }
344 |
345 | // Helper macro that does the actual implementation
346 | macro_rules! impl_ruby_version_with_module {
347 | ($struct_name:ident, $module:ident) => {
348 | pub struct $struct_name;
349 |
350 | impl RubyApiCompat for $struct_name {
351 | impl_iseq_functions!(rbspy_ruby_structs::$module::rb_iseq_struct);
352 | impl_thread_functions!(rbspy_ruby_structs::$module::rb_thread_t);
353 | impl_control_frame_functions!(
354 | rbspy_ruby_structs::$module::rb_control_frame_struct,
355 | rbspy_ruby_structs::$module::rb_execution_context_struct
356 | );
357 | impl_ruby_str_to_rust_str!(rbspy_ruby_structs::$module::RString);
358 | }
359 | };
360 | }
361 |
362 | // Ruby 3.1.x implementations
363 | impl_ruby_version!(Ruby310);
364 | impl_ruby_version!(Ruby311);
365 | impl_ruby_version!(Ruby312);
366 | impl_ruby_version!(Ruby313);
367 | impl_ruby_version!(Ruby314);
368 | impl_ruby_version!(Ruby315);
369 | impl_ruby_version!(Ruby316);
370 | impl_ruby_version!(Ruby317);
371 |
372 | // Ruby 3.2.x implementations
373 | impl_ruby_version!(Ruby320);
374 | impl_ruby_version!(Ruby321);
375 | impl_ruby_version!(Ruby322);
376 | impl_ruby_version!(Ruby323);
377 | impl_ruby_version!(Ruby324);
378 | impl_ruby_version!(Ruby325);
379 | impl_ruby_version!(Ruby326);
380 | impl_ruby_version!(Ruby327);
381 | impl_ruby_version!(Ruby328);
382 |
383 | // Ruby 3.3.x implementations
384 | impl_ruby_version!(Ruby330);
385 | impl_ruby_version!(Ruby331);
386 | impl_ruby_version!(Ruby332);
387 | impl_ruby_version!(Ruby333);
388 | impl_ruby_version!(Ruby334);
389 | impl_ruby_version!(Ruby335);
390 | impl_ruby_version!(Ruby336);
391 | impl_ruby_version!(Ruby337);
392 | impl_ruby_version!(Ruby338);
393 |
394 | // Ruby 3.4.x implementations
395 | impl_ruby_version!(Ruby340);
396 | impl_ruby_version!(Ruby341);
397 | impl_ruby_version!(Ruby342);
398 | impl_ruby_version!(Ruby343);
399 | impl_ruby_version!(Ruby344);
400 |
401 | // Main API struct
402 | pub struct RubyAPI {
403 | inner: Box,
404 | }
405 |
406 | impl RubyAPI {
407 | pub fn new(version: RubyVersion) -> Self {
408 | let inner: Box = match version {
409 | // Ruby 3.1.x
410 | RubyVersion::Ruby310 => Box::new(Ruby310),
411 | RubyVersion::Ruby311 => Box::new(Ruby311),
412 | RubyVersion::Ruby312 => Box::new(Ruby312),
413 | RubyVersion::Ruby313 => Box::new(Ruby313),
414 | RubyVersion::Ruby314 => Box::new(Ruby314),
415 | RubyVersion::Ruby315 => Box::new(Ruby315),
416 | RubyVersion::Ruby316 => Box::new(Ruby316),
417 | RubyVersion::Ruby317 => Box::new(Ruby317),
418 |
419 | // Ruby 3.2.x
420 | RubyVersion::Ruby320 => Box::new(Ruby320),
421 | RubyVersion::Ruby321 => Box::new(Ruby321),
422 | RubyVersion::Ruby322 => Box::new(Ruby322),
423 | RubyVersion::Ruby323 => Box::new(Ruby323),
424 | RubyVersion::Ruby324 => Box::new(Ruby324),
425 | RubyVersion::Ruby325 => Box::new(Ruby325),
426 | RubyVersion::Ruby326 => Box::new(Ruby326),
427 | RubyVersion::Ruby327 => Box::new(Ruby327),
428 | RubyVersion::Ruby328 => Box::new(Ruby328),
429 |
430 | // Ruby 3.3.x
431 | RubyVersion::Ruby330 => Box::new(Ruby330),
432 | RubyVersion::Ruby331 => Box::new(Ruby331),
433 | RubyVersion::Ruby332 => Box::new(Ruby332),
434 | RubyVersion::Ruby333 => Box::new(Ruby333),
435 | RubyVersion::Ruby334 => Box::new(Ruby334),
436 | RubyVersion::Ruby335 => Box::new(Ruby335),
437 | RubyVersion::Ruby336 => Box::new(Ruby336),
438 | RubyVersion::Ruby337 => Box::new(Ruby337),
439 | RubyVersion::Ruby338 => Box::new(Ruby338),
440 |
441 | // Ruby 3.4.x
442 | RubyVersion::Ruby340 => Box::new(Ruby340),
443 | RubyVersion::Ruby341 => Box::new(Ruby341),
444 | RubyVersion::Ruby342 => Box::new(Ruby342),
445 | RubyVersion::Ruby343 => Box::new(Ruby343),
446 | RubyVersion::Ruby344 => Box::new(Ruby344),
447 | };
448 |
449 | RubyAPI { inner }
450 | }
451 |
452 | pub unsafe fn get_iseq_info(&self, iseq_addr: u64) -> (Option, Option) {
453 | self.inner.get_iseq_info(iseq_addr)
454 | }
455 |
456 | pub unsafe fn get_first_lineno(&self, iseq_addr: u64) -> VALUE {
457 | self.inner.get_first_lineno(iseq_addr)
458 | }
459 |
460 | pub unsafe fn get_label(&self, iseq_addr: u64) -> VALUE {
461 | self.inner.get_label(iseq_addr)
462 | }
463 |
464 | pub unsafe fn get_base_label(&self, iseq_addr: u64) -> VALUE {
465 | self.inner.get_base_label(iseq_addr)
466 | }
467 |
468 | pub unsafe fn is_iseq_imemo(&self, iseq_ptr: *const c_void) -> bool {
469 | self.inner.is_iseq_imemo(iseq_ptr)
470 | }
471 |
472 | pub unsafe fn get_ec_from_thread(&self, thread_val: VALUE) -> *mut c_void {
473 | self.inner.get_ec_from_thread(thread_val)
474 | }
475 |
476 | #[inline]
477 | pub unsafe fn iterate_frame_iseqs(&self, ec_val: VALUE, frame_handler: &mut dyn FnMut(u64)) {
478 | self.inner.iterate_frame_iseqs(ec_val, frame_handler)
479 | }
480 | }
481 |
482 | unsafe fn get_ruby_version_string() -> String {
483 | let version_sym = rb_sys::rb_intern("RUBY_VERSION\0".as_ptr() as *const c_char);
484 | let version_val = rb_sys::rb_const_get(rb_sys::rb_cObject, version_sym);
485 |
486 | let version_ptr = rb_sys::rb_string_value_cstr(&version_val as *const _ as *mut _);
487 | let version_cstr = CStr::from_ptr(version_ptr);
488 | version_cstr.to_string_lossy().to_string()
489 | }
490 |
491 | pub fn detect_ruby_version() -> RubyVersion {
492 | unsafe {
493 | let version_str = get_ruby_version_string();
494 |
495 | // Ruby 3.1.x
496 | if version_str.starts_with("3.1.0") {
497 | RubyVersion::Ruby310
498 | } else if version_str.starts_with("3.1.1") {
499 | RubyVersion::Ruby311
500 | } else if version_str.starts_with("3.1.2") {
501 | RubyVersion::Ruby312
502 | } else if version_str.starts_with("3.1.3") {
503 | RubyVersion::Ruby313
504 | } else if version_str.starts_with("3.1.4") {
505 | RubyVersion::Ruby314
506 | } else if version_str.starts_with("3.1.5") {
507 | RubyVersion::Ruby315
508 | } else if version_str.starts_with("3.1.6") {
509 | RubyVersion::Ruby316
510 | } else if version_str.starts_with("3.1.7") {
511 | RubyVersion::Ruby317
512 |
513 | // Ruby 3.2.x
514 | } else if version_str.starts_with("3.2.0") {
515 | RubyVersion::Ruby320
516 | } else if version_str.starts_with("3.2.1") {
517 | RubyVersion::Ruby321
518 | } else if version_str.starts_with("3.2.2") {
519 | RubyVersion::Ruby322
520 | } else if version_str.starts_with("3.2.3") {
521 | RubyVersion::Ruby323
522 | } else if version_str.starts_with("3.2.4") {
523 | RubyVersion::Ruby324
524 | } else if version_str.starts_with("3.2.5") {
525 | RubyVersion::Ruby325
526 | } else if version_str.starts_with("3.2.6") {
527 | RubyVersion::Ruby326
528 | } else if version_str.starts_with("3.2.7") {
529 | RubyVersion::Ruby327
530 | } else if version_str.starts_with("3.2.8") {
531 | RubyVersion::Ruby328
532 |
533 | // Ruby 3.3.x
534 | } else if version_str.starts_with("3.3.0") {
535 | RubyVersion::Ruby330
536 | } else if version_str.starts_with("3.3.1") {
537 | RubyVersion::Ruby331
538 | } else if version_str.starts_with("3.3.2") {
539 | RubyVersion::Ruby332
540 | } else if version_str.starts_with("3.3.3") {
541 | RubyVersion::Ruby333
542 | } else if version_str.starts_with("3.3.4") {
543 | RubyVersion::Ruby334
544 | } else if version_str.starts_with("3.3.5") {
545 | RubyVersion::Ruby335
546 | } else if version_str.starts_with("3.3.6") {
547 | RubyVersion::Ruby336
548 | } else if version_str.starts_with("3.3.7") {
549 | RubyVersion::Ruby337
550 | } else if version_str.starts_with("3.3.8") {
551 | RubyVersion::Ruby338
552 |
553 | // Ruby 3.4.x
554 | } else if version_str.starts_with("3.4.0") {
555 | RubyVersion::Ruby340
556 | } else if version_str.starts_with("3.4.1") {
557 | RubyVersion::Ruby341
558 | } else if version_str.starts_with("3.4.2") {
559 | RubyVersion::Ruby342
560 | } else if version_str.starts_with("3.4.3") {
561 | RubyVersion::Ruby343
562 | } else if version_str.starts_with("3.4.4") {
563 | RubyVersion::Ruby344
564 | } else {
565 | panic!("Unknown Ruby version: {}", version_str);
566 | }
567 | }
568 | }
569 |
--------------------------------------------------------------------------------
/ext/sdb/src/stack_scanner.rs:
--------------------------------------------------------------------------------
1 | use crate::helpers::*;
2 | use crate::iseq_logger::*;
3 | use crate::ruby_version::*;
4 | use crate::trace_id::*;
5 | use std::sync::atomic::AtomicU64;
6 |
7 | use chrono::Utc;
8 | use libc::c_void;
9 | use rb_sys::{
10 | rb_gc_mark, rb_num2dbl, rb_thread_call_with_gvl, rb_thread_call_without_gvl, Qnil, Qtrue,
11 | RARRAY_LEN, VALUE,
12 | };
13 |
14 | use sysinfo::System;
15 |
16 | use std::collections::{HashMap, HashSet};
17 | use std::time::Duration;
18 | use std::{ptr, thread};
19 |
20 | use lazy_static::lazy_static;
21 | use spin::Mutex;
22 | use std::sync;
23 | use std::sync::Condvar;
24 |
25 | const ONE_MILLISECOND_NS: u64 = 1_000_000; // 1ms in nanoseconds
26 |
27 | lazy_static! {
28 | // For using raw mutex in Ruby, we need to release GVL before acquiring the lock.
29 | // Spinlock is simpler and in scanner which acquires and releases the lock quit fast.
30 | // The only potential issue is that Ruby may suspend the thread for a long time, for example GC.
31 | // I am not sure this could happen and even if it could happen, it should extremely rare.
32 | // So, I think it is good choice to use spinlock here
33 | pub static ref STACK_SCANNER: Mutex = Mutex::new(StackScanner::new());
34 | pub static ref START_TO_PULL_COND_VAR: (sync::Mutex, Condvar) = (sync::Mutex::new(true), Condvar::new());
35 | pub static ref RUBY_API: RubyAPI = RubyAPI::new(detect_ruby_version());
36 | }
37 |
38 | pub struct StackScanner {
39 | should_stop: bool,
40 | ecs: Vec,
41 | threads: Vec,
42 | sleep_nanos: u64,
43 | iseq_logger: IseqLogger,
44 | pause: bool,
45 | iseq_buffer: HashSet,
46 | translated_iseq: HashMap,
47 | }
48 |
49 | impl StackScanner {
50 | pub fn new() -> Self {
51 | StackScanner {
52 | should_stop: false,
53 | ecs: Vec::new(),
54 | threads: Vec::new(),
55 | sleep_nanos: 0,
56 | iseq_logger: IseqLogger::new(),
57 | pause: false,
58 | iseq_buffer: HashSet::new(),
59 | translated_iseq: HashMap::new(),
60 | }
61 | }
62 |
63 | #[inline]
64 | pub fn pause(&mut self) {
65 | self.pause = true;
66 | }
67 |
68 | #[inline]
69 | pub fn resume(&mut self) {
70 | self.pause = false;
71 | }
72 |
73 | #[inline]
74 | pub fn is_paused(&self) -> bool {
75 | self.pause
76 | }
77 |
78 | #[inline]
79 | pub fn stop(&mut self) {
80 | self.should_stop = true;
81 | self.iseq_logger.flush();
82 | }
83 |
84 | #[inline]
85 | pub fn is_stopped(&self) -> bool {
86 | self.should_stop
87 | }
88 |
89 | #[inline]
90 | pub fn mark_iseqs(&mut self) {
91 | unsafe {
92 | for (iseq, _) in &self.translated_iseq {
93 | rb_gc_mark(*iseq);
94 | }
95 | }
96 | }
97 |
98 | #[inline]
99 | pub fn consume_iseq_buffer(&mut self) {
100 | unsafe {
101 | for iseq in self.iseq_buffer.drain() {
102 | let iseq_ptr = iseq as usize as *const c_void;
103 |
104 | // Ruby VM pushes non-IMEMO_ISEQ iseqs to the frame,
105 | // such as captured->code.ifunc in vm_yield_with_cfunc func,
106 | // we do not handle those for now.
107 | if !RUBY_API.is_iseq_imemo(iseq_ptr) {
108 | continue;
109 | }
110 |
111 | let (label_str, path_str) = RUBY_API.get_iseq_info(iseq);
112 |
113 | self.iseq_logger.log(&format!(
114 | "[symbol] {}, {}, {}",
115 | iseq,
116 | label_str.unwrap_or("".to_string()),
117 | path_str.unwrap_or("".to_string())
118 | ));
119 | self.translated_iseq.insert(iseq, true);
120 | }
121 |
122 | self.iseq_logger.flush();
123 | }
124 | }
125 |
126 | // GVL must be hold before calling this function
127 | pub unsafe fn update_threads(&mut self, threads_to_scan: VALUE, current_thread: VALUE) {
128 | let threads_count = RARRAY_LEN(threads_to_scan) as isize;
129 | self.threads = [].to_vec();
130 | self.ecs = [].to_vec();
131 |
132 | let mut i: isize = 0;
133 | while i < threads_count {
134 | let thread = rb_sys::rb_ary_entry(threads_to_scan, i as i64);
135 |
136 | if thread != current_thread && thread != (Qnil as VALUE) {
137 | self.threads.push(thread);
138 | let ec = RUBY_API.get_ec_from_thread(thread);
139 | self.ecs.push(ec as VALUE);
140 | }
141 |
142 | i += 1;
143 | }
144 | }
145 | }
146 |
147 | #[inline]
148 | // Caller needs to guarantee the thread is alive until the end of this function
149 | unsafe extern "C" fn record_thread_frames(
150 | thread_val: VALUE,
151 | ec_val: VALUE,
152 | trace_table: &HashMap,
153 | stack_scanner: &mut StackScanner,
154 | ) -> bool {
155 | let trace_id = get_trace_id(trace_table, thread_val);
156 | let ts = Utc::now().timestamp_micros();
157 |
158 | stack_scanner.iseq_logger.push(trace_id);
159 | stack_scanner.iseq_logger.push(ts as u64);
160 |
161 | // Use the new closure-based API
162 | let mut frame_handler = |iseq_addr: u64| {
163 | if iseq_addr == 0 {
164 | return;
165 | } else {
166 | stack_scanner.iseq_buffer.insert(iseq_addr);
167 | stack_scanner.iseq_logger.push(iseq_addr);
168 | }
169 | };
170 |
171 | RUBY_API.iterate_frame_iseqs(ec_val, &mut frame_handler);
172 | stack_scanner.iseq_logger.push_seperator();
173 |
174 | true
175 | }
176 |
177 | extern "C" fn ubf_pull_loop(_: *mut c_void) {
178 | let mut stack_scanner = STACK_SCANNER.lock();
179 | stack_scanner.stop();
180 | }
181 |
182 | // eBPF only has uptime, this function returns both uptime and clock time for converting
183 | #[inline]
184 | pub(crate) fn uptime_and_clock_time() -> (u64, i64) {
185 | let uptime = System::uptime();
186 |
187 | // as uptime's accuracy is 1s, use busy loop to get the next right second,
188 | // and then the clock time for converting between uptime and clock time
189 | loop {
190 | if System::uptime() - uptime >= 1.0 as u64 {
191 | // covert to micros for uptime
192 | return (
193 | (uptime + 1.0 as u64) * 1_000_000,
194 | Utc::now().timestamp_micros(),
195 | );
196 | }
197 | }
198 | }
199 |
200 | #[inline]
201 | // co-work with pull_loop
202 | unsafe extern "C" fn looping_helper() -> bool {
203 | let trace_table = get_trace_id_table();
204 |
205 | loop {
206 | let mut i = 0;
207 |
208 | let mut stack_scanner = STACK_SCANNER.lock();
209 | // when acquire the lock, check the scanner has been paused or not
210 | if stack_scanner.is_paused() {
211 | // pause this looping by return, false means pause the scanner
212 | return false;
213 | }
214 |
215 | let len = stack_scanner.ecs.len();
216 | let sleep_nanos = stack_scanner.sleep_nanos;
217 |
218 | if stack_scanner.is_stopped() {
219 | // stop this looping by return, true means stop the scanner
220 | return true;
221 | }
222 |
223 | while i < len {
224 | let ec = stack_scanner.ecs[i];
225 | let thread = stack_scanner.threads[i];
226 | record_thread_frames(thread, ec, trace_table, &mut stack_scanner);
227 | i += 1;
228 | }
229 |
230 | // It only drops the lock after all threads are scanned,
231 | // as ruby doesn't have many threads normally and stack scanning is very fast.
232 | drop(stack_scanner);
233 |
234 | if sleep_nanos < ONE_MILLISECOND_NS {
235 | // For sub-millisecond sleeps, use busy-wait for more precise timing
236 | let start = std::time::Instant::now();
237 | while start.elapsed().as_nanos() < sleep_nanos as u128 {
238 | std::hint::spin_loop();
239 | }
240 | } else {
241 | thread::sleep(Duration::from_nanos(sleep_nanos / 10 * 9));
242 | }
243 | }
244 | }
245 |
246 | unsafe extern "C" fn consume_iseq_buffer_with_gvl(_: *mut c_void) -> *mut c_void {
247 | let mut stack_scanner = STACK_SCANNER.lock();
248 | stack_scanner.consume_iseq_buffer();
249 | ptr::null_mut()
250 | }
251 |
252 | unsafe extern "C" fn pull_loop(_: *mut c_void) -> *mut c_void {
253 | loop {
254 | let (start_to_pull_lock, cvar) = &*START_TO_PULL_COND_VAR;
255 | let mut start = start_to_pull_lock.lock().unwrap();
256 |
257 | while !*start {
258 | start = cvar.wait(start).unwrap();
259 | }
260 | drop(start);
261 |
262 | // looping until the gc pauses the scanner
263 | let should_stop = looping_helper();
264 |
265 | if should_stop {
266 | rb_thread_call_with_gvl(Some(consume_iseq_buffer_with_gvl), ptr::null_mut());
267 | return ptr::null_mut();
268 | }
269 | }
270 | }
271 |
272 | pub(crate) unsafe extern "C" fn rb_pull(_module: VALUE, sleep_seconds: VALUE) -> VALUE {
273 | log::debug!(
274 | "[scanner][main] start to pull sleep_seconds = {:?}",
275 | sleep_seconds
276 | );
277 |
278 | let sleep_nanos = (rb_num2dbl(sleep_seconds) * 1_000_000_000.0) as u64;
279 |
280 | let mut stack_scanner = STACK_SCANNER.lock();
281 | stack_scanner.sleep_nanos = sleep_nanos;
282 | drop(stack_scanner);
283 |
284 | println!("sleep interval {:?} ns", sleep_nanos / 1000);
285 |
286 | // release gvl for avoiding block application's threads
287 | rb_thread_call_without_gvl(
288 | Some(pull_loop),
289 | ptr::null_mut(),
290 | Some(ubf_pull_loop),
291 | ptr::null_mut(),
292 | );
293 |
294 | let mut stack_scanner = STACK_SCANNER.lock();
295 | stack_scanner.consume_iseq_buffer();
296 |
297 | Qtrue as VALUE
298 | }
299 |
300 | pub(crate) unsafe extern "C" fn rb_log_uptime_and_clock_time(_module: VALUE) -> VALUE {
301 | let (uptime, clock_time) = uptime_and_clock_time();
302 | log::info!("[time] uptime={:?}, clock_time={:?}", uptime, clock_time);
303 |
304 | return Qnil as VALUE;
305 | }
306 |
307 | pub(crate) unsafe extern "C" fn rb_update_threads_to_scan(
308 | module: VALUE,
309 | threads_to_scan: VALUE,
310 | ) -> VALUE {
311 | let argv: &[VALUE; 0] = &[];
312 | let current_thread = call_method(module, "current_thread", 0, argv);
313 |
314 | let mut stack_scanner = STACK_SCANNER.lock();
315 | stack_scanner.update_threads(threads_to_scan, current_thread);
316 | drop(stack_scanner);
317 |
318 | return Qnil as VALUE;
319 | }
320 |
321 | pub(crate) unsafe extern "C" fn rb_stop_scanner(_module: VALUE) -> VALUE {
322 | let mut stack_scanner = STACK_SCANNER.lock();
323 | stack_scanner.stop();
324 |
325 | return Qnil as VALUE;
326 | }
327 |
328 | // for testing
329 | pub(crate) unsafe extern "C" fn rb_get_on_stack_func_addresses(
330 | _module: VALUE,
331 | _thread_val: VALUE,
332 | ) -> VALUE {
333 | // TODO: Implement using the new RubyAPI
334 | let ary = rb_sys::rb_ary_new_capa(0);
335 | ary
336 | }
337 |
--------------------------------------------------------------------------------
/ext/sdb/src/tester.rs:
--------------------------------------------------------------------------------
1 | use libc::c_void;
2 | use rb_sys::{rb_ary_new, rb_ary_push, rb_int2inum, rb_num2long, rb_str_new, Qfalse, Qtrue, VALUE};
3 |
4 | pub(crate) unsafe extern "C" fn rb_get_ec_from_thread(_module: VALUE, thread: VALUE) -> VALUE {
5 | let ec = crate::stack_scanner::RUBY_API.get_ec_from_thread(thread) as isize;
6 | rb_int2inum(ec)
7 | }
8 |
9 | pub(crate) unsafe extern "C" fn rb_get_iseqs(_module: VALUE, ec_val: VALUE) -> VALUE {
10 | let ec = rb_num2long(ec_val) as *const c_void as u64;
11 | let array = rb_ary_new();
12 |
13 | crate::stack_scanner::RUBY_API.iterate_frame_iseqs(ec, &mut |iseq_addr| {
14 | rb_ary_push(array, rb_int2inum(iseq_addr as isize));
15 | });
16 |
17 | array
18 | }
19 |
20 | pub(crate) unsafe extern "C" fn rb_is_iseq_imemo(_module: VALUE, iseq_val: VALUE) -> VALUE {
21 | let iseq = rb_num2long(iseq_val) as *const c_void;
22 |
23 | if crate::stack_scanner::RUBY_API.is_iseq_imemo(iseq) {
24 | Qtrue.into()
25 | } else {
26 | Qfalse.into()
27 | }
28 | }
29 |
30 | unsafe fn rust_to_ruby_string(rust_str: &str) -> VALUE {
31 | rb_str_new(rust_str.as_ptr() as *const i8, rust_str.len() as i64)
32 | }
33 |
34 | pub(crate) unsafe extern "C" fn rb_get_iseq_info(_module: VALUE, iseq_val: VALUE) -> VALUE {
35 | let iseq = rb_num2long(iseq_val) as *const c_void;
36 | let (label, path) = crate::stack_scanner::RUBY_API.get_iseq_info(iseq as u64);
37 |
38 | let rb_label = rust_to_ruby_string(&label.unwrap_or("".to_string()));
39 | let rb_path = rust_to_ruby_string(&path.unwrap_or("".to_string()));
40 |
41 | let array = rb_ary_new();
42 |
43 | rb_ary_push(array, rb_label);
44 | rb_ary_push(array, rb_path);
45 |
46 | array
47 | }
48 |
--------------------------------------------------------------------------------
/ext/sdb/src/trace_id.rs:
--------------------------------------------------------------------------------
1 | use rb_sys::{rb_num2ulong, Qfalse, Qtrue, VALUE};
2 | use std::collections::HashMap;
3 | use std::ptr;
4 | use std::sync::atomic::{AtomicU64, Ordering};
5 |
6 | static mut TRACE_TABLE: *mut HashMap = ptr::null_mut();
7 |
8 | fn init_trace_id_table() {
9 | unsafe {
10 | if TRACE_TABLE.is_null() {
11 | let map = Box::new(HashMap::new());
12 | TRACE_TABLE = Box::into_raw(map);
13 | }
14 | }
15 | }
16 |
17 | // Safety Argument:
18 |
19 | // If a hash-map has a fixed size, it's relatively "safe" to access it without a lock.
20 | // Only during rehashing, it needs to avoid all reads at the same time.
21 |
22 | // When the Ruby VM creates a new thread, SDB inserts a dummy value into the trace-id table.
23 | // At this moment, it already acquired the STACK_SCANNER, which blocks the scanner thread -- the only reader (see rb_add_thread_to_scan method).
24 | // This guarantees that no reader is accessing this table during rehashing.
25 |
26 | // Additionally, when SDB needs to read this, it uses a memory barrier for getting the newest value.
27 | // Therefore, I believe this implementation is safe even though it has a lot of "unsafe" code. Yes, it is tricky.
28 | #[inline]
29 | pub(crate) fn get_trace_id_table() -> &'static mut HashMap {
30 | unsafe {
31 | if TRACE_TABLE.is_null() {
32 | init_trace_id_table();
33 | }
34 |
35 | &mut *TRACE_TABLE
36 | }
37 | }
38 |
39 | #[inline]
40 | pub(crate) fn set_trace_id(thread: VALUE, trace_id: u64) -> bool {
41 | let trace_table = get_trace_id_table();
42 | let thread_id = thread as u64;
43 |
44 | trace_table
45 | .entry(thread_id)
46 | .or_insert_with(|| AtomicU64::new(trace_id))
47 | .store(trace_id, Ordering::Release);
48 | true
49 | }
50 |
51 | // When we use a memory order, do we need to consider a function as unsafe?
52 | #[inline]
53 | pub(crate) fn get_trace_id(trace_table: &HashMap, thread: VALUE) -> u64 {
54 | trace_table
55 | .get(&thread)
56 | .map(|atomic| atomic.load(Ordering::Acquire))
57 | .unwrap_or(0)
58 | }
59 |
60 | pub(crate) unsafe extern "C" fn rb_set_trace_id(
61 | _module: VALUE,
62 | thread: VALUE,
63 | trace_id: VALUE,
64 | ) -> VALUE {
65 | if set_trace_id(thread, rb_num2ulong(trace_id)) {
66 | Qtrue as VALUE
67 | } else {
68 | Qfalse as VALUE
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/sdb.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "sdb/version"
4 | require_relative "sdb/sdb"
5 | require_relative "sdb/puma_patch"
6 | require_relative "sdb/thread_patch"
7 |
8 | module Sdb
9 | class << self
10 | def init
11 | current_version = Gem::Version.new(RUBY_VERSION)
12 | min_version = Gem::Version.new('3.1.0')
13 | max_version = Gem::Version.new('3.4.4')
14 |
15 | unless current_version >= min_version && current_version <= max_version
16 | raise "Unsupported ruby version: #{RUBY_VERSION}. Supported versions: > 3.1.0 and <= 3.4.4"
17 | end
18 |
19 | self.log_uptime_and_clock_time
20 | @initialized = true
21 | @active_threads = []
22 | @lock = Mutex.new
23 | @scan_config = {}
24 | self.setup_gc_hooks
25 | end
26 |
27 | def current_thread
28 | @current_thread ||= Thread.current
29 | end
30 |
31 | def log_gvl_addr
32 | log_gvl_addr_for_thread(Thread.current)
33 | end
34 |
35 | def busy_pull(threads)
36 | self.pull(threads, 0)
37 | end
38 |
39 | def start_scan_helper(sleep_interval, &filter)
40 | @scan_config = { sleep_interval: sleep_interval, filter: filter }
41 |
42 | # Don't start thread in master process
43 | if puma_detected? && puma_worker_mode?
44 | config = Puma.cli_config
45 | config.options[:before_worker_boot] ||= []
46 | config.options[:before_worker_boot] << proc {
47 | Sdb.worker_forked!
48 | }
49 |
50 | config.options[:before_worker_shutdown] ||= []
51 | config.options[:before_worker_shutdown] << proc {
52 | Sdb.stop_scanner
53 | @scanner_thread.join # wait scanner finishes its work
54 | }
55 | else
56 | start_scanning
57 | end
58 | end
59 |
60 | def scan_all_threads(sleep_interval = 0.001)
61 | start_scan_helper(sleep_interval) { true }
62 | end
63 |
64 | def scan_puma_threads(sleep_interval = 0.001)
65 | start_scan_helper(sleep_interval) do |thread|
66 | thread.name&.include?('puma srv tp')
67 | end
68 | end
69 |
70 | def thread_created(thread)
71 | @lock.synchronize do
72 | @active_threads << thread
73 | if @scan_config[:filter]
74 | threads_to_scan = @active_threads.filter(&@scan_config[:filter]).to_a
75 | self.update_threads_to_scan(threads_to_scan)
76 | end
77 | end
78 | end
79 |
80 | def thread_deleted(thread)
81 | @lock.synchronize do
82 | @active_threads.delete(thread)
83 | if @scan_config[:filter]
84 | threads_to_scan = @active_threads.filter(&@scan_config[:filter]).to_a
85 | self.update_threads_to_scan(threads_to_scan)
86 | end
87 | end
88 | end
89 |
90 | def worker_forked!
91 | start_scanning if @scan_config
92 | end
93 |
94 | private
95 |
96 | def puma_detected?
97 | defined?(Puma) && (defined?(Puma::Server) || defined?(Puma::Cluster))
98 | end
99 |
100 | def puma_worker_mode?
101 | Puma.respond_to?(:cli_config) && Puma.cli_config.options[:workers].to_i > 0
102 | end
103 |
104 | def start_scanning
105 | self.init_logger
106 |
107 | @lock.synchronize do
108 | threads_to_scan = @active_threads.filter(&@scan_config[:filter]).to_a
109 | self.update_threads_to_scan(threads_to_scan)
110 | end
111 |
112 | @scanner_thread = Thread.new do
113 | Thread.current.name = "sdb-scanner-#{Process.pid}"
114 |
115 | self.pull(@scan_config[:sleep_interval])
116 | end
117 | end
118 | end
119 | end
120 |
121 | Sdb.init
122 |
123 | module ThreadInitializePatch
124 | def initialize(*args, &block)
125 | old_block = block
126 |
127 | block = ->() do
128 | Sdb.thread_created(Thread.current)
129 | result = old_block.call(*args)
130 | Sdb.thread_deleted(Thread.current)
131 | result
132 | end
133 |
134 | super(&block)
135 | end
136 | end
137 |
138 | Thread.prepend(ThreadInitializePatch)
139 |
--------------------------------------------------------------------------------
/lib/sdb/puma_patch.rb:
--------------------------------------------------------------------------------
1 | require 'cpu_time'
2 |
3 | module Sdb
4 | module PumaPatch
5 | class << self
6 | attr_accessor :logger
7 |
8 | def patch(logger)
9 | Puma::Server.prepend(HandleRequest)
10 | self.logger = logger
11 | end
12 | end
13 |
14 | module HandleRequest
15 | def handle_request(client, requests)
16 | t0 = Time.now
17 | cpu_time0 = CPUTime.time
18 | trace_id = client.env['HTTP_TRACE_ID'].to_i
19 | Sdb.set_trace_id(Thread.current, trace_id)
20 | Thread.current[:sdb] = {}
21 | rv = super
22 | t1 = Time.now
23 | cpu_time1 = CPUTime.time
24 | Sdb::PumaPatch.logger.info "[SDB][puma-delay]: trace_id=#{trace_id}, thread_id=#{Thread.current.native_thread_id}, remote_port=#{client.io.peeraddr[1]}, start_ts=#{t0.to_f * 1_000_000}, end_ts=#{t1.to_f * 1_000_000}, delay=#{(t1 - t0) * 1000} ms, cpu_time=#{(cpu_time1 - cpu_time0) * 1000 } ms, status=#{Thread.current[:sdb][:status]}"
25 |
26 | rv
27 | ensure
28 | Sdb.set_trace_id(Thread.current, 0)
29 | Thread.current[:sdb] = {}
30 | end
31 |
32 | def prepare_response(status, headers, res_body, requests, client)
33 | Thread.current[:sdb][:status] = status
34 |
35 | super
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/sdb/thread_patch.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Sdb
4 | module ThreadPatch
5 | def self.patch
6 | Thread.prepend(Initialize)
7 | end
8 |
9 | module Initialize
10 | def initialize(*args, &block)
11 | parent = Thread.current
12 |
13 | child = super
14 | puts "[#{Process.pid}] parent_id=#{parent.native_thread_id}, child_id=#{child.native_thread_id}, caller=#{caller[0]}"
15 | child
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/sdb/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Sdb
4 | VERSION = "0.1.0"
5 | end
6 |
--------------------------------------------------------------------------------
/scripts/benchmark.js:
--------------------------------------------------------------------------------
1 | import http from "k6/http";
2 |
3 | const url = __ENV.URL || 'http://localhost:3000/api/v3/topics';
4 |
5 | export const options = {
6 | vus: 1,
7 | duration: '30s',
8 | };
9 |
10 | export default function () {
11 | const params = {
12 | headers: {
13 | 'Content-Type': 'application/json',
14 | 'Accept': 'application/json',
15 | },
16 | };
17 |
18 | http.get(url, params);
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/benchmark/README.md:
--------------------------------------------------------------------------------
1 | ## Basic Setup
2 |
3 | - Target application: A forked Homeland(Ruby-China forum)
4 | - Repo: https://github.com/yfractal/homelandx
5 | - Branches
6 | - main branch for baseline and rbspy
7 | - feat-sdb for SDB
8 | - feat-vernier for vernier 1.0.0 patched version
9 | - Puma Start Command
10 | - WEB_CONCURRENCY=0 RAILS_MAX_THREADS=2 RAILS_ENV=production bundle exec puma -p 3000
11 | - Sampling Rate: 1000 times per second
12 |
13 |
14 | ## Stack Profilers Setup
15 | ### SDB
16 |
17 | ```ruby
18 | Thread.new do
19 | sleep 5
20 |
21 | threads = Thread.list.filter {|thread| thread.name&.include?('puma srv tp') }
22 |
23 | threads.each do |thread|
24 | puts "[#{thread.native_thread_id}] #{thread.name}"
25 | end
26 |
27 | Sdb.scan_puma_threads(0.001) # set sampling rate to 1000 times per second
28 | end
29 | ```
30 |
31 | ### Patched Vernier 1.0.0
32 | The code is on https://github.com/yfractal/vernier v1.0.0-patch branch
33 |
34 | For compariation, I use `usleep(1000)` to avoid busy pull and removed GC event hook, see https://github.com/yfractal/vernier/pull/1/files
35 |
36 | And enable it through patching puma `handle_servers` method, see https://github.com/yfractal/homelandx/blob/feat-vernier/config/initializers/vernier.rb
37 |
38 | ```ruby
39 | module PumaPatch
40 | def self.patch
41 | Puma::Server.class_eval do
42 | alias_method :old_handle_servers, :handle_servers
43 |
44 | def handle_servers
45 | Vernier.trace(out: "rails.json", hooks: [:rails], interval: 1000, allocation_interval: 0) do |collector|
46 | old_handle_servers
47 | end
48 | end
49 | end
50 | end
51 | end
52 |
53 | PumaPatch.patch
54 | ```
55 |
56 | ### rbspy
57 | rbspy is started by `./target/release/rbspy record --rate 1000 --pid --nonblocking`
58 | TODO rbspy version
59 |
60 | ## Benchmark Process
61 |
62 | ## Install k6
63 | sudo dnf install https://dl.k6.io/rpm/repo.rpm
64 | sudo dnf install -y k6
65 |
66 | ## Sending Requests
67 | URL=http://ip-172-31-27-62.ap-southeast-1.compute.internal:3000/api/v3/topics k6 run benchmark.js
68 |
69 | It sends requests for 30 seconds in one thread(uv) and such throughput consumes around 60% CPU of on CPU core.
70 |
71 | ## CPU Usage
72 | top -b -n 200 -d 0.1 -p > top_output.txt
73 | awk 'NR % 9 == 8' top_output.txt | awk '{print $9}' | tail -n 100 | awk '{sum += $1; count++} END {print sum / count}'
74 |
75 | It collects 20 seconds data and calculate the last 10 seconds average CPU usage.
76 |
77 | ## Request Delay
78 | As puma server needs warm up, we only take the last 100 requests into consideration.
79 |
80 | grep "puma-delay" homeland.log | tail -n 100 > tail-100.log
81 |
82 | ## Analysing
83 |
84 | ```ruby
85 | analyzer = Sdb::Analyzer::Puma.new('tail-100.log')
86 | data = analyzer.read
87 | puts analyzer.statistic(data)
88 | ```
89 |
90 | ## Stack Profiler Impact Measurement
91 | Since SDB and Vernier are run within the Ruby application, we first need to run Homeland without any profilers to establish a baseline.
92 |
93 | Next, enable one stack profiler and run the same test again.
94 |
95 | Finally, calculate the stack profiler’s impact by subtracting the baseline result from the test result with the profiler enabled.
96 |
--------------------------------------------------------------------------------
/scripts/gc_compact.rb:
--------------------------------------------------------------------------------
1 | require 'sdb'
2 |
3 | @a = []
4 | def xxxxxx
5 | sleep 0.01
6 | puts "[#{Process.pid}] native_thread_id=#{Thread.current.native_thread_id}"
7 | end
8 |
9 | def bbbbbb
10 | @a = [1] * rand(1000)
11 | puts Thread.list
12 | xxxxxx
13 | end
14 |
15 | def cccccc
16 | @a = "1" * rand(1000)
17 | bbbbbb
18 | end
19 |
20 | def dddddd
21 | @a = "a" * rand(1000)
22 | cccccc
23 | end
24 |
25 | def fffffff
26 | dddddd
27 | end
28 |
29 | Thread.new do
30 | sleep 0.5
31 | 100.times do
32 | fffffff
33 | end
34 |
35 | x = [rand] * 10013
36 |
37 | puts x
38 | puts "compact result = #{GC.compact}"
39 |
40 | 100.times do
41 | fffffff
42 | end
43 |
44 |
45 | y = "abc" * 10013
46 | puts y
47 | puts "compact result = #{GC.compact}"
48 |
49 | 100.times do
50 | fffffff
51 | end
52 |
53 | puts "compact result = #{GC.compact}"
54 |
55 | loop do
56 | fffffff
57 | # puts "compact result = #{GC.compact}"
58 | end
59 | end
60 |
61 | Thread.new do
62 | Sdb.scan_all_threads
63 | end
64 |
65 | loop do
66 | sleep 1
67 | puts "looping"
68 | fffffff
69 | end
--------------------------------------------------------------------------------
/scripts/handle_thread_reclaim.rb:
--------------------------------------------------------------------------------
1 | require 'byebug'
2 | require 'sdb'
3 |
4 | 10.times do
5 | Thread.new {
6 | sleep rand(10)
7 | }
8 | end
9 |
10 | thread = Thread.new {
11 | Sdb.scan_all_threads
12 | }
13 |
14 | thread.join
15 |
--------------------------------------------------------------------------------
/scripts/on_cpu_example.rb:
--------------------------------------------------------------------------------
1 | def xxxxxx
2 | puts "[#{Process.pid}] native_thread_id=#{Thread.current.native_thread_id}"
3 | end
4 |
5 | def b
6 | puts Thread.list
7 | xxxxxx
8 | end
9 |
10 | def c
11 | b
12 | end
13 |
14 | def d
15 | c
16 | end
17 |
18 | def fffffff
19 | sleep 1
20 | d
21 | end
22 |
23 | Thread.new do
24 | Thread.current.name = "test-bbbb"
25 | loop do
26 | sleep 1
27 | fffffff
28 | end
29 | end
30 |
31 | Thread.current.name = "test-aaaaaaaaa"
32 |
33 | loop do
34 | sleep 1
35 | puts "looping"
36 | fffffff
37 | end
--------------------------------------------------------------------------------
/scripts/rails_demo.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 | require 'uri'
3 |
4 | `mkdir /log`
5 |
6 | fork { `python3 /sdb/symbolizer/symbolizer.py > /log/symbols.log` }
7 |
8 | sleep 1
9 |
10 | fork { `WEB_CONCURRENCY=0 RAILS_MAX_THREADS=2 RAILS_ENV=production bundle exec puma -p 3000 > /log/puma.log` }
11 |
12 | class Requester
13 | def initialize
14 | @trace_id = 10000
15 | end
16 |
17 | def request
18 | uri = URI.parse("http://localhost:3000/")
19 | request = Net::HTTP::Get.new(uri)
20 | request["Trace-id"] = @trace_id
21 |
22 | response = Net::HTTP.start(uri.hostname, uri.port) do |http|
23 | http.request(request)
24 | end
25 |
26 | puts "Response code: #{response.code}"
27 | puts "Response body: #{response.body}"
28 |
29 | @trace_id += 1
30 | end
31 | end
32 |
33 | sleep 5
34 |
35 | requester = Requester.new
36 |
37 | # warm up
38 | 5.times do
39 | requester.request
40 | end
41 |
42 | 10.times do
43 | requester.request
44 | end
45 |
--------------------------------------------------------------------------------
/scripts/tcp.py:
--------------------------------------------------------------------------------
1 | from bcc import BPF
2 | from socket import inet_ntop, AF_INET, AF_INET6
3 | from struct import pack
4 | import argparse
5 |
6 | # inspired by bcc tcpconnlat
7 |
8 | # arguments
9 | examples = """examples:
10 | ./tcpconnlat # trace all TCP connect()s
11 | ./tcpconnlat -p 181 # only trace PID 181
12 | """
13 | parser = argparse.ArgumentParser(
14 | description="Trace TCP life time",
15 | formatter_class=argparse.RawDescriptionHelpFormatter,
16 | epilog=examples)
17 | parser.add_argument("-p", "--pid", help="trace this PID only")
18 | parser.add_argument("-v", "--verbose", action="store_true", help="print the BPF program for debugging purposes")
19 | args = parser.parse_args()
20 |
21 | debug = 0
22 |
23 | # define BPF program
24 | bpf_text = """
25 | #include
26 | #include
27 | #include
28 | #include
29 |
30 | struct info_t {
31 | u64 ts;
32 | u32 pid;
33 | };
34 | BPF_HASH(start, struct sock *, struct info_t);
35 |
36 | // separate data structs for ipv4 and ipv6
37 | struct ipv4_data_t {
38 | u32 tgid; // group id or process id
39 | u32 pid;
40 | u32 daddr;
41 | u64 ip;
42 | u16 lport;
43 | u16 dport;
44 | u64 start_ts;
45 | u64 end_ts;
46 | };
47 | BPF_PERF_OUTPUT(ipv4_events);
48 |
49 | struct ipv6_data_t {
50 | u32 tgid; // group id or process id
51 | u32 pid;
52 | unsigned __int128 daddr;
53 | u64 ip;
54 | u16 lport;
55 | u16 dport;
56 | u64 start_ts;
57 | u64 end_ts;
58 | };
59 | BPF_PERF_OUTPUT(ipv6_events);
60 |
61 | int trace_tcp_connect(struct pt_regs *ctx, struct sock *sk)
62 | {
63 | u32 pid = bpf_get_current_pid_tgid() >> 32;
64 | FILTER
65 | struct info_t info = {};
66 | info.pid = pid;
67 | info.ts = bpf_ktime_get_ns();
68 | start.update(&sk, &info);
69 | return 0;
70 | };
71 |
72 | int trace_tcp_close(struct pt_regs *ctx, struct sock *skp)
73 | {
74 | struct info_t *infop = start.lookup(&skp);
75 | if (infop == 0) {
76 | return 0; // missed entry or filtered
77 | }
78 |
79 | u64 ts = infop->ts;
80 | u64 now = bpf_ktime_get_ns();
81 | u64 tgid = bpf_get_current_pid_tgid() >> 32;
82 |
83 | u16 family = 0, lport = 0, dport = 0;
84 | family = skp->__sk_common.skc_family;
85 | lport = skp->__sk_common.skc_num;
86 | dport = skp->__sk_common.skc_dport;
87 |
88 | // emit to appropriate data path
89 | if (family == AF_INET) {
90 | struct ipv4_data_t data4 = {};
91 | data4.tgid = tgid;
92 | data4.pid = infop->pid;
93 | data4.ip = 4;
94 | data4.daddr = skp->__sk_common.skc_daddr;
95 | data4.lport = lport;
96 | data4.dport = ntohs(dport);
97 | data4.start_ts = ts;
98 | data4.end_ts = now;
99 | ipv4_events.perf_submit(ctx, &data4, sizeof(data4));
100 |
101 | } else /* AF_INET6 */ {
102 | struct ipv6_data_t data6 = {};
103 | data6.tgid = tgid;
104 | data6.pid = infop->pid;
105 | data6.ip = 6;
106 | bpf_probe_read_kernel(&data6.daddr, sizeof(data6.daddr),
107 | skp->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
108 | data6.lport = lport;
109 | data6.dport = ntohs(dport);
110 | data6.start_ts = ts;
111 | data6.end_ts = now;
112 | ipv6_events.perf_submit(ctx, &data6, sizeof(data6));
113 | }
114 |
115 | start.delete(&skp);
116 |
117 | return 0;
118 | }
119 | """
120 |
121 | # code substitutions
122 | if args.pid:
123 | bpf_text = bpf_text.replace('FILTER',
124 | 'if (pid != %s) { return 0; }' % args.pid)
125 | else:
126 | bpf_text = bpf_text.replace('FILTER', '')
127 | if debug or args.verbose:
128 | print(bpf_text)
129 | if args.ebpf:
130 | exit()
131 |
132 | # initialize BPF
133 | b = BPF(text=bpf_text)
134 |
135 | b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_tcp_connect")
136 | b.attach_kprobe(event="tcp_v6_connect", fn_name="trace_tcp_connect")
137 | b.attach_kprobe(event="tcp_close", fn_name="trace_tcp_close")
138 |
139 | # process event
140 | def print_ipv4_event(cpu, data, size):
141 | event = b["ipv4_events"].event(data)
142 | daddr = inet_ntop(AF_INET, pack("I", event.daddr))
143 | print(f"tgid={event.tgid}, pid={event.pid}, ip=#{event.ip}, addr={daddr}, lport={event.lport}, dport={event.dport}, start_ts={event.start_ts}, end_ts={event.end_ts}")
144 |
145 | def print_ipv6_event(cpu, data, size):
146 | event = b["ipv6_events"].event(data)
147 | daddr = inet_ntop(AF_INET6, event.daddr)
148 | print(f"tgid={event.tgid}, pid={event.pid}, ip=#{event.ip}, addr={daddr}, lport={event.lport}, dport={event.dport}, start_ts={event.start_ts}, end_ts={event.end_ts}")
149 |
150 | # read events
151 | b["ipv4_events"].open_perf_buffer(print_ipv4_event)
152 | b["ipv6_events"].open_perf_buffer(print_ipv6_event)
153 |
154 | while 1:
155 | try:
156 | b.perf_buffer_poll()
157 | except KeyboardInterrupt:
158 | exit()
159 |
--------------------------------------------------------------------------------
/scripts/thread_schedule.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from bcc import BPF
5 | from sys import stderr
6 |
7 | MAX_STR_LENGTH = 128
8 |
9 | bpf_text = """
10 | #include
11 | #include
12 |
13 | struct event_t {
14 | u32 tgid; // group id or process id
15 | u32 pid; // thread id
16 | char name[TASK_COMM_LEN];
17 | u64 start_ts;
18 | u64 end_ts;
19 | };
20 |
21 | BPF_HASH(events_map, u32, struct event_t);
22 | BPF_PERF_OUTPUT(events);
23 |
24 | int oncpu(struct pt_regs *ctx, struct task_struct *prev) {
25 | u32 pid, tgid;
26 | u64 ts = bpf_ktime_get_ns();
27 |
28 | // current task
29 | u64 pid_tgid = bpf_get_current_pid_tgid();
30 | tgid = pid_tgid >> 32;
31 | pid = (__u32)pid_tgid;
32 |
33 | if (FILTER) {
34 | struct event_t event = {};
35 | event.pid = pid;
36 | event.tgid = tgid;
37 | bpf_get_current_comm(&event.name, sizeof(event.name));
38 | event.start_ts = ts;
39 | events_map.update(&tgid, &event);
40 | }
41 |
42 | // pre task
43 | pid = prev->pid;
44 | tgid = prev->tgid;
45 | if (FILTER) {
46 | struct event_t *eventp = events_map.lookup(&tgid);
47 | if (eventp == 0) {
48 | bpf_trace_printk("prev is nil");
49 | return 0;
50 | }
51 | eventp->end_ts = ts;
52 | events.perf_submit(ctx, eventp, sizeof(*eventp));
53 | }
54 |
55 | return 0;
56 | }
57 | """
58 | args = sys.argv[1:]
59 |
60 | if args == []:
61 | condition = '1'
62 | else:
63 | condition = ' || '.join([f'tgid == {i}' for i in args])
64 |
65 | bpf_text = bpf_text.replace('FILTER', condition)
66 |
67 | # initialize BPF
68 | b = BPF(text=bpf_text)
69 | b.attach_kprobe(event_re=r'^finish_task_switch$|^finish_task_switch\.isra\.\d$',
70 | fn_name="oncpu")
71 | matched = b.num_open_kprobes()
72 | if matched == 0:
73 | print("error: 0 functions traced. Exiting.", file=stderr)
74 | exit()
75 |
76 | def print_event(cpu, data, size):
77 | event = b["events"].event(data)
78 | print(f"tgid={event.tgid}, pid={event.pid}, name={event.name.decode('utf-8')}, start_ts={event.start_ts}, end_ts={event.end_ts}")
79 |
80 | b["events"].open_perf_buffer(print_event)
81 | while 1:
82 | b.perf_buffer_poll()
83 |
--------------------------------------------------------------------------------
/sdb-shim/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sdb-shim"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [dependencies]
10 | libc = "0.2.155"
11 | libloading = "0.8.5"
12 | fast_log = "1.7.3"
13 | log = "0.4.22"
14 |
--------------------------------------------------------------------------------
/sdb-shim/src/lib.rs:
--------------------------------------------------------------------------------
1 | extern crate libc;
2 | extern crate libloading;
3 |
4 | use fast_log::config::Config;
5 | use libc::{clock_gettime, pthread_cond_t, pthread_mutex_t, timespec, CLOCK_MONOTONIC};
6 | use libloading::Library;
7 | use std::sync::Once;
8 |
9 | static INIT: Once = Once::new();
10 | static mut REAL_PTHREAD_MUTEX_LOCK: Option i32> =
11 | None;
12 | static mut REAL_PTHREAD_MUTEX_UNLOCK: Option i32> =
13 | None;
14 |
15 | static mut REAL_PTHREAD_COND_WAIT: Option<
16 | unsafe extern "C" fn(cond: *mut pthread_cond_t, *mut pthread_mutex_t) -> i32,
17 | > = None;
18 |
19 | static mut REAL_PTHREAD_COND_SIGNAL: Option<
20 | unsafe extern "C" fn(cond: *mut pthread_cond_t) -> i32,
21 | > = None;
22 |
23 | unsafe fn init_once() {
24 | INIT.call_once(|| {
25 | fast_log::init(Config::new().file("sdb-lock.log").chan_len(Some(1_000_000))).unwrap();
26 |
27 | let lib = Library::new("libpthread.so.0").expect("Failed to load libpthread");
28 |
29 | let lock_func: libloading::Symbol i32> = lib
30 | .get(b"pthread_mutex_lock")
31 | .expect("Failed to load pthread_mutex_lock symbol");
32 | REAL_PTHREAD_MUTEX_LOCK = Some(*lock_func);
33 |
34 | let unlock_func: libloading::Symbol i32> =
35 | lib.get(b"pthread_mutex_unlock")
36 | .expect("Failed to load pthread_mutex_unlock symbol");
37 | REAL_PTHREAD_MUTEX_UNLOCK = Some(*unlock_func);
38 |
39 | let cond_wait_func: libloading::Symbol<
40 | unsafe extern "C" fn(cond: *mut pthread_cond_t, *mut pthread_mutex_t) -> i32,
41 | > = lib
42 | .get(b"pthread_cond_wait")
43 | .expect("Failed to load pthread_cond_wait symbol");
44 | REAL_PTHREAD_COND_WAIT = Some(*cond_wait_func);
45 |
46 | let cond_signal_func: libloading::Symbol<
47 | unsafe extern "C" fn(cond: *mut pthread_cond_t) -> i32,
48 | > = lib
49 | .get(b"pthread_cond_signal")
50 | .expect("Failed to load pthread_cond_signal symbol");
51 | REAL_PTHREAD_COND_SIGNAL = Some(*cond_signal_func);
52 | });
53 | }
54 |
55 | fn get_linux_thread_id() -> libc::pid_t {
56 | unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t }
57 | }
58 |
59 | fn ts() -> u64 {
60 | let mut ts: timespec = timespec {
61 | tv_sec: 0,
62 | tv_nsec: 0,
63 | };
64 |
65 | let result = unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts) };
66 | if result == 0 {
67 | ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64
68 | } else {
69 | 0
70 | }
71 | }
72 |
73 | #[no_mangle]
74 | pub unsafe extern "C" fn pthread_mutex_lock(mutex: *mut pthread_mutex_t) -> i32 {
75 | init_once();
76 |
77 | let tid = get_linux_thread_id();
78 | log::info!(
79 | "[lock][mutex][acquire]: thread={}, lock_addr={}",
80 | tid,
81 | mutex as u64
82 | );
83 |
84 | if let Some(real_pthread_mutex_lock) = REAL_PTHREAD_MUTEX_LOCK {
85 | let ret = real_pthread_mutex_lock(mutex);
86 | log::info!(
87 | "[lock][mutex][acquired]: thread={}, lock_addr={}, ts={}",
88 | tid,
89 | mutex as u64,
90 | ts()
91 | );
92 |
93 | ret
94 | } else {
95 | eprintln!("Failed to resolve pthread_mutex_lock");
96 | -1
97 | }
98 | }
99 |
100 | #[no_mangle]
101 | pub unsafe extern "C" fn pthread_mutex_unlock(mutex: *mut pthread_mutex_t) -> i32 {
102 | init_once();
103 |
104 | let tid = get_linux_thread_id();
105 |
106 | if let Some(real_pthread_mutex_unlock) = REAL_PTHREAD_MUTEX_UNLOCK {
107 | let ret = real_pthread_mutex_unlock(mutex);
108 | if ret == 0 {
109 | log::info!(
110 | "[lock][mutex][unlock]: thread={}, lock_addr={}, ts={}",
111 | tid,
112 | mutex as u64,
113 | ts()
114 | );
115 | }
116 |
117 | ret
118 | } else {
119 | eprintln!("Failed to resolve pthread_mutex_unlock");
120 | -1
121 | }
122 | }
123 |
124 | #[no_mangle]
125 | pub unsafe extern "C" fn pthread_cond_wait(
126 | cond: *mut pthread_cond_t,
127 | mutex: *mut pthread_mutex_t,
128 | ) -> i32 {
129 | init_once();
130 |
131 | let tid = get_linux_thread_id();
132 | log::info!(
133 | "[lock][cond][acquire]: thread={}, lock_addr={}, cond_var_addr={}, ts={}",
134 | tid,
135 | mutex as u64,
136 | cond as u64,
137 | ts()
138 | );
139 |
140 | if let Some(real_pthread_cond_wait) = REAL_PTHREAD_COND_WAIT {
141 | let ret = real_pthread_cond_wait(cond, mutex);
142 | if ret == 0 {
143 | log::info!(
144 | "[lock][cond][acquired]: thread={}, lock_addr={}, cond_var_addr={}, ts={}",
145 | tid,
146 | mutex as u64,
147 | cond as u64,
148 | ts()
149 | );
150 | }
151 |
152 | ret
153 | } else {
154 | eprintln!("Failed to resolve real_pthread_cond_wait");
155 | -1
156 | }
157 | }
158 |
159 | #[no_mangle]
160 | pub unsafe extern "C" fn pthread_cond_signal(cond: *mut pthread_cond_t) -> i32 {
161 | init_once();
162 |
163 | let tid = get_linux_thread_id();
164 |
165 | if let Some(real_pthread_cond_signal) = REAL_PTHREAD_COND_SIGNAL {
166 | let ret = real_pthread_cond_signal(cond);
167 | if ret == 0 {
168 | log::info!(
169 | "[lock][cond][signal]: thread={}, cond_var_addr={}, ts={}",
170 | tid,
171 | cond as u64,
172 | ts()
173 | );
174 | }
175 |
176 | ret
177 | } else {
178 | eprintln!("Failed to resolve real_pthread_cond_wait");
179 | -1
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/sdb.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/sdb/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "sdb"
7 | spec.version = Sdb::VERSION
8 | spec.authors = ["Mike Yang"]
9 | spec.email = ["yfractal@gmail.com"]
10 |
11 | spec.summary = "High-accuracy stack profiling for Ruby."
12 | spec.description = "High-accuracy stack profiling for Ruby."
13 | spec.required_ruby_version = ">= 3.0.0"
14 | spec.required_rubygems_version = ">= 3.3.3"
15 |
16 | spec.metadata["source_code_uri"] = "https://github.com/yfractal/sdb"
17 |
18 | # Specify which files should be added to the gem when it is released.
19 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20 | gemspec = File.basename(__FILE__)
21 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
22 | ls.readlines("\x0", chomp: true).reject do |f|
23 | (f == gemspec) ||
24 | f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
25 | end
26 | end
27 | spec.bindir = "exe"
28 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29 | spec.require_paths = ["lib"]
30 | spec.extensions = ["ext/sdb/Cargo.toml"]
31 |
32 | # Uncomment to register a new dependency of your gem
33 | # spec.add_dependency "example-gem", "~> 1.0"
34 | spec.add_dependency 'cpu_time'
35 | spec.add_development_dependency 'byebug'
36 | spec.add_runtime_dependency 'puma', '>= 6.0'
37 |
38 | # For more information and examples about making a new gem, check out our
39 | # guide at: https://bundler.io/guides/creating_gem.html
40 | end
41 |
--------------------------------------------------------------------------------
/sig/sdb.rbs:
--------------------------------------------------------------------------------
1 | module Sdb
2 | VERSION: String
3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4 | end
5 |
--------------------------------------------------------------------------------
/spec/ruby_version_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | def foo
4 | bar
5 | end
6 |
7 | def bar
8 | sleep 1_000_000
9 | end
10 | RSpec.describe 'RubyVersion' do
11 | it 'Get execution context from thread' do
12 | thread = Thread.new { sleep 1_000_000 }
13 | ec = SdbTester.ec_from_thread(thread)
14 | thread.kill
15 | expect(ec).not_to eq nil
16 | end
17 |
18 | it 'Get iseqs from execution context' do
19 | thread = Thread.new { foo }
20 | ec = SdbTester.ec_from_thread(thread)
21 | sleep 0.1
22 | iseqs = SdbTester.iseqs_from_ec(ec)
23 | expect(iseqs.count).to be >= 2
24 | thread.kill
25 | end
26 |
27 | it 'Test is_iseq_imemo' do
28 | thread = Thread.new { foo }
29 | ec = SdbTester.ec_from_thread(thread)
30 | sleep 0.1
31 | iseqs = SdbTester.iseqs_from_ec(ec)
32 | is_imemo = iseqs.map do |iseq|
33 | SdbTester.is_iseq_imemo(iseq)
34 | end
35 |
36 | expect(is_imemo).to eq [false, true, true, true, true, false]
37 | thread.kill
38 | end
39 |
40 | it 'Get Iseq Info' do
41 | thread = Thread.new { foo }
42 | ec = SdbTester.ec_from_thread(thread)
43 | sleep 0.1
44 | iseqs = SdbTester.iseqs_from_ec(ec)
45 |
46 | expect(SdbTester.iseq_info(iseqs[1])).to eq ['bar', __FILE__]
47 | expect(SdbTester.iseq_info(iseqs[2])).to eq ['foo', __FILE__]
48 | expect(SdbTester.iseq_info(iseqs[3])).to eq ['block (3 levels) in ', __FILE__]
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "sdb"
4 | require "byebug"
5 |
6 | RSpec.configure do |config|
7 | # Enable flags like --only-failures and --next-failure
8 | config.example_status_persistence_file_path = ".rspec_status"
9 |
10 | # Disable RSpec exposing methods globally on `Module` and `main`
11 | config.disable_monkey_patching!
12 |
13 | config.expect_with :rspec do |c|
14 | c.syntax = :expect
15 | end
16 | end
17 |
--------------------------------------------------------------------------------