dirge-agent 0.4.1

Minimalistic coding agent written in Rust, optimized for memory footprint and performance
# dap-profiler — sampling profiler driven by DAP Janet FFI
#
# Uses timer-based sampling: hooks on-tool-end and periodically fires
# `dap/stack-trace` against the active session, aggregating callers
# by function name. After N samples it prints a top-N hotspot report.
#
# Architecture:
#   `/dap-profile start <interval-ms>` arms the profiler.
#   Each tick calls `dap/stack-trace` (Janet FFI -> DapSessionManager
#   -> DAP stackTrace request -> adapter -> real stack frames).
#   Frames are aggregated by function name. Over many samples,
#   frequently-called functions dominate the report.
#   `/dap-profile stop` prints the report and resets.
#   `/dap-profile report` prints current report without stopping.
#
# This is a STATISTICAL profiler — same algorithm as `perf record`.

(def hooks ["on-tool-end"])

# ── State ────────────────────────────────────────────────────────────

(var profiling false)
(var profile-next-sample 0)  # epoch ms of next sample
(var profile-interval 200)   # ms between samples
(var profile-max-samples 100)
(var profile-samples 0)
(var profile-counts @{})     # frame-name -> count

# ── Hook — sample on each tool-end if profiling ──────────────────────

# Monotonic-ish millisecond clock. `os/clock` returns seconds as a double;
# `os/time` is whole seconds, which made the `profile-interval` (ms) math
# off by ~1000x (a "200ms" profile sampled every 200s).
(defn- now-ms [] (* (os/clock) 1000))

(defn on-tool-end [ctx]
  (when (and profiling (>= (now-ms) profile-next-sample))
    (set profile-next-sample (+ (now-ms) profile-interval))
    (take-sample)))

# ── Sample logic ─────────────────────────────────────────────────────

(defn- take-sample []
  (def frames-str (dap/stack-trace))
  (when frames-str
    # Janet has no JSON parser, so match function names with regex.
    # The stack-trace JSON has '"name": "function_name"' patterns.
    (var idx 0)
    (var found 0)
    (while (and (< found 10)
                (>= idx 0)
                (< idx (length frames-str)))
      (def name-start (string/find "\"name\": \"" frames-str idx))
      (if (not name-start)
        (break))
      (set name-start (+ name-start 9))
      (def name-end (string/find "\"" frames-str name-start))
      (when name-end
        (def name (string/slice frames-str name-start name-end))
        # Skip runtime/launcher frames
        (when (and (not (string/find "_run_" name))
                   (not (string/find "runpy" name))
                   (not= name "<module>")
                   (not= name "_run_code")
                   (not= name "_run_module_as_main"))
          (def count (get profile-counts name 0))
          (put profile-counts name (+ count 1))
          (set found (+ found 1)))
        (set idx (+ name-end 1)))))
  (set profile-samples (+ profile-samples 1))
  (when (>= profile-samples profile-max-samples)
    (harness/notify (profiler-report) :info)
    (set profiling false)))

# ── Report generator ────────────────────────────────────────────────

(defn- profiler-report []
  (def entries @[])
  (loop [[k v] :pairs profile-counts]
    (array/push entries [v k]))
  (sort entries (fn [a b] (> (get a 0) (get b 0))))
  (var out "PROFILER REPORT\n")
  (set out (string out "Samples: " profile-samples
                   "  Interval: " profile-interval "ms\n\n"))
  (var rank 0)
  (loop [entry :in entries]
    (when (< rank 20)
      (def count (get entry 0))
      (def key (get entry 1))
      (def pct (math/round (* 100 (/ count profile-samples))))
      (set out (string out (string "  " rank ". " pct "%  " key "\n")))
      (set rank (+ rank 1))))
  out)

# ── Slash commands ──────────────────────────────────────────────────

(defn profile-start [args]
  (when (not (dap/session-active?))
    (break "No active DAP session — launch a program first"))
  (def interval (if (empty? args) 200 (math/parse-int args)))
  (set profiling true)
  (set profile-interval (max 50 interval))
  (set profile-max-samples 100)
  (set profile-samples 0)
  (set profile-next-sample (+ (now-ms) profile-interval))
  (set profile-counts @{})
  (string "Profiling started — " profile-max-samples
          " samples at " profile-interval "ms intervals"))

(defn profile-stop [_args]
  (when (not profiling) (break "Profiler not running"))
  (set profiling false)
  (profiler-report))

(defn profile-report [_args]
  (if profiling (profiler-report)
    "Profiler not running — start with /dap-profile"))

(defn profile-clear [_args]
  (set profile-counts @{})
  (set profile-samples 0)
  "Profile data cleared")

(harness/register-command "dap-profile" "profile-start")
(harness/register-command "dap-profile-stop" "profile-stop")
(harness/register-command "dap-profile-report" "profile-report")
(harness/register-command "dap-profile-clear" "profile-clear")