├── .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 | Image 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 | ![roda](https://github.com/yfractal/sdb-analyzer/blob/main/images/roda.png) 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 | --------------------------------------------------------------------------------