{"id":50329093,"url":"https://github.com/threez/prof.cr","last_synced_at":"2026-05-29T08:31:00.254Z","repository":{"id":356546236,"uuid":"1232994045","full_name":"threez/prof.cr","owner":"threez","description":"Sampling profiler for Crystal language ","archived":false,"fork":false,"pushed_at":"2026-05-08T16:09:06.000Z","size":21,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-08T16:16:26.981Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/threez.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-08T13:32:05.000Z","updated_at":"2026-05-08T16:09:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/threez/prof.cr","commit_stats":null,"previous_names":["threez/prof.cr"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/threez/prof.cr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fprof.cr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fprof.cr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fprof.cr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fprof.cr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/threez","download_url":"https://codeload.github.com/threez/prof.cr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/threez%2Fprof.cr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33644125,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-29T08:30:54.984Z","updated_at":"2026-05-29T08:31:00.244Z","avatar_url":"https://github.com/threez.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# prof.cr\n\nA sampling CPU profiler for Crystal programs. Installs a `SIGPROF` handler\nvia `sigaction(2)`, fires it on a CPU-time interval set with `setitimer(2)`,\nand captures the call stack into a pre-allocated buffer on every tick. After\nstopping, addresses are resolved to symbol names and the result is a\n`Prof::Report` you can inspect or export.\n\n## Platform support\n\n| Platform | Stack unwinding | Symbol resolution |\n|---|---|---|\n| macOS | `backtrace(3)` (libSystem) | `backtrace_symbols(3)` — full Crystal method names |\n| Linux (glibc) | `backtrace(3)` (glibc) | `backtrace_symbols(3)` — full Crystal method names |\n| Linux (musl) | **not supported** | — |\n| FreeBSD | libunwind | `dladdr(3)` — type name only |\n| Solaris | libunwind | `dladdr(3)` — type name only |\n| QNX | libunwind | `dladdr(3)` — type name only |\n| HP-UX | libunwind | `dladdr(3)` — type name only |\n\n`ITIMER_PROF` fires on CPU time (user + kernel), so sleeping or waiting on I/O\ndoes not accumulate samples.\n\n### Dependencies\n\n### No extra dependencies\n\nmacOS (libSystem) and Linux/glibc (glibc) provide `backtrace(3)` out of the\nbox. FreeBSD and other platforms use Crystal's bundled LLVM libunwind. No\nextra packages are needed on any supported platform.\n\nmusl libc is **not supported**: musl ≤1.2.5 provides no `backtrace(3)`, and\nAlpine 3.22 ships neither libunwind nor libexecinfo, making reliable signal-\nhandler stack collection impossible without additional system packages that are\nno longer available. The library raises a compile-time error on musl.\n\n## Installation\n\nAdd the dependency to your `shard.yml`:\n\n```yaml\ndependencies:\n  prof:\n    github: threez/prof.cr\n```\n\nThen run `shards install`.\n\n## Usage\n\n### Block form (recommended)\n\n```crystal\nrequire \"prof\"\n\nreport = Prof.profile(interval: 1.millisecond) do\n  my_expensive_computation\nend\n\nputs report                          # top-10 hottest frames to stdout\nreport.to_speedscope(\"profile.json\") # open at https://www.speedscope.app\nreport.to_folded(\"profile.folded\")   # pipe to flamegraph.pl or import into speedscope\n```\n\n### Manual start / stop\n\n```crystal\nProf.start(interval: 1.millisecond)\n\ndo_phase_one\ndo_phase_two\n\nreport = Prof.stop\nputs report\n```\n\n### Options\n\n```crystal\nProf.profile(\n  interval:    1.millisecond,   # sampling interval (CPU time, min ~100µs)\n  max_samples: 100_000,         # hard cap on number of samples collected\n  max_depth:   64               # max stack frames captured per sample\n) { ... }\n```\n\n### Reading the report\n\n```crystal\nreport.total_samples          # number of samples captured\nreport.samples                # Array(Array(Prof::Frame))\nreport.top(10)                # Array({Frame, Int32}) sorted by sample count\n\nreport.samples.each do |stack|\n  stack.each { |frame| puts \"#{frame.name}  0x#{frame.address.to_s(16)}\" }\nend\n```\n\n### Exporting\n\n| Method | Format | Viewer |\n|---|---|---|\n| `report.to_speedscope(\"out.json\")` | speedscope sampled profile | [speedscope.app](https://www.speedscope.app) |\n| `report.to_folded(\"out.folded\")` | folded stacks | `flamegraph.pl` or speedscope import |\n\nBoth methods also accept an `IO` instead of a path.\n\n## How it works\n\n1. `Prof.start` allocates two `LibC.malloc` buffers (frame addresses + depths),\n   installs a raw `sigaction` handler with `SA_RESTART | SA_SIGINFO`, and arms\n   `setitimer(ITIMER_PROF, ...)`.\n2. On each `SIGPROF`, the handler captures the current call stack into the\n   pre-allocated buffer — no heap allocation, no Crystal runtime calls:\n   - **macOS / Linux+glibc**: calls `backtrace(3)` from libSystem / glibc.\n   - **FreeBSD / others**: calls `_Unwind_Backtrace` via Crystal's stdlib\n     `LibUnwind` bindings.\n3. `Prof.stop` disarms the timer, restores the previous signal handler, then\n   resolves the captured addresses to symbol strings:\n   - **macOS / Linux+glibc**: `backtrace_symbols(3)` in a single batch call;\n     first 3 frames (backtrace call, signal handler, OS trampoline) are discarded.\n   - **FreeBSD / others**: `dladdr(3)` per address, returning `sym+0xoffset`; first\n     2 frames (signal handler, OS trampoline) are discarded.\n4. Symbols are parsed into `Prof::Frame` structs and wrapped in a `Prof::Report`.\n\nThe signal handler is a non-capturing Crystal lambda assigned to `sa_sigaction`.\nBecause it only reads and writes class-level static variables (no heap\nallocation, no closure), Crystal converts it to a plain C function pointer —\nthe same technique Crystal's own `Signal.trap` infrastructure uses internally.\n\n## Limitations\n\n### Thread safety\n\n`ITIMER_PROF` is process-wide; only one profiler session can run at a time. The\nprofiler is **not safe** for use with `-Dpreview_mt` (multi-threaded Crystal):\nthe sample counter is a plain `Int32` incremented without an atomic operation,\nso concurrent signal delivery can corrupt the count.\n\n### Source location resolution (`--release`)\n\nSource file and line numbers require both DWARF debug info and `addr2line` on\nPATH. `crystal run` and `crystal build` (without `--release`) include debug info\nby default. `crystal build --release` strips it, so `frame.file` and\n`frame.line` will be `nil` — the profiler still collects samples and resolves\nfunction names, but without source locations.\n\n`addr2line` is usually available via the `binutils` package on Linux. On macOS\nit is not installed by default; install it with `brew install binutils` or\nensure the LLVM toolchain's `addr2line` is on PATH.\n\n## Development\n\n```sh\ncrystal spec                        # run the test suite\ncrystal build src/prof.cr           # type-check the library (no output = success)\ncrystal spec spec/prof_spec.cr:42   # run a single test by line number\n```\n\nThe spec uses `@[NoInline]` functions and a multi-million-iteration workload to\nensure the timer fires several times per test run across the range of machines.\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/threez/prof.cr/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Open a Pull Request\n\n## Contributors\n\n- [Vincent Landgraf](https://github.com/threez) - creator and maintainer\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthreez%2Fprof.cr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthreez%2Fprof.cr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthreez%2Fprof.cr/lists"}