Contents

Building a Zero-Dependency AST Profiler in Pure Coni

Performance optimization is critical, but profiling dynamic languages often comes with a massive caveat: runtime overhead. When dealing with interpreters, developers usually resort to mocking or wrapping functions dynamically at runtime using constructs like with-redefs.

However, when you need your profiler to work flawlessly in both a Native interpreter and Ahead-Of-Time (AOT) compiled WebAssembly environments, dynamic runtime rebinding simply isn’t an option. AOT compilers resolve functions statically.

So, how do you profile an application without modifying the compiler itself or suffering runtime penalties?

You rewrite the Abstract Syntax Tree (AST) at compile time.

The profile-block Macro

We built a pure-Coni profiler that leverages Lisp’s ultimate superpower: code as data (homoiconicity). Instead of modifying function behavior at runtime, we implemented a profile-block macro that intercepts the application’s AST before it is evaluated.

It recursively walks the tree, detects every defn statement, and injects nanosecond-resolution sys-time-now timing wrappers directly into the function body.

Because the code is modified structurally at compile-time, it executes blazingly fast natively, and perfectly translates into statically compiled Wasm modules.

The Code Example

Here is how you use it. You simply require the profiler and wrap your application logic inside (profile-block ...):

(require "libs/profiler/src/profiler.coni" :all)

(println "Initializing pure Coni profiler on application code...\n")

;; 1. Wrap the target program blocks in `profile-block`
(profile-block

  ;; All defns inside this block will be automatically rewritten with high-res timers
  
  (defn compute-fibonacci "Calculates Fibonacci number inefficiently" [n]
    (if (< n 2)
      n
      (+ (compute-fibonacci (- n 1)) 
         (compute-fibonacci (- n 2)))))
         
  (defn string-ops "Does some string operations" [s]
    (dotimes [i 1000]
      (let [x (str s " - " i)]
        x)))

  (defn run-app []
    (println "Running compute-fibonacci(15)...")
    (let [res (compute-fibonacci 15)]
      (println "Result:" res))
      
    (println "Running string-ops...")
    (string-ops "hello")))

;; 2. Run the application
(run-app)

;; 3. Print the detailed timing report
(print-profiles!)

The Profiler in Action

When we execute the above code using the native Coni interpreter, we get incredibly precise metrics, seamlessly aggregating deep recursive functions like compute-fibonacci:

nico@Karabiner-Nicolas coni-lang % ./coni libs/profiler/examples/example.coni

Initializing pure Coni profiler on application code...

Running compute-fibonacci(15)...
Result: 610
Running string-ops...

========================================
⏱️  CONI PROFILER REPORT
========================================
- run-app:
  Calls      : 1
  Total Time : 11.363 ms
  Avg Time   : 11.363 ms
- compute-fibonacci:
  Calls      : 1973
  Total Time : 88.284 ms
  Avg Time   : 0.044746 ms
- string-ops:
  Calls      : 1
  Total Time : 2.122 ms
  Avg Time   : 2.122 ms
========================================

Because the timing injection is baked deeply into the AST, compute-fibonacci accurately reports all 1,973 recursive invocations, averaging an incredibly fast 0.044 ms per call!

This demonstrates the sheer power of Lisp macros in the Coni ecosystem. With just a single file of pure Coni code, we created an enterprise-grade profiler capable of targeting completely different runtime architectures.