---
title: "Benchmarks"
output: arl::arl_html_vignette
pkgdown:
  as_is: true
vignette: >
  %\VignetteIndexEntry{Benchmarks}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
arl::register_knitr_engine()

# Read benchmark data checked out by `make bench-data` (or `make vignettes`).
bench_json <- tryCatch({
  data_file <- "benchmarks/results/data.js"
  if (!file.exists(data_file)) data_file <- file.path("..", data_file)
  raw <- readLines(data_file, warn = FALSE)
  json_text <- sub("^window\\.BENCHMARK_DATA = ", "", paste(raw, collapse = "\n"))
  jsonlite::fromJSON(json_text)
}, error = function(e) NULL)

if (!is.null(bench_json)) {
  runs <- bench_json$entries$Benchmark
  # runs is a data.frame; benches column is a list of data.frames
  n_runs <- nrow(runs)
  benches <- runs$benches[[n_runs]]
  # Also grab the first run for before/after comparisons
  if (n_runs > 1) {
    first_benches <- runs$benches[[1L]]
  } else {
    first_benches <- NULL
  }
}
has_bench_data <- !is.null(bench_json)
```

This page summarizes Arl's performance characteristics.
For full profiling details and optimization history, see
[`benchmarks/PERFORMANCE.md`](https://github.com/wwbrannon/arl/blob/main/benchmarks/PERFORMANCE.md)
in the repository.

## Pipeline Overview

Every Arl expression passes through five stages:

```
Source → Tokenizer → Parser → Macro Expander → Compiler → R eval() → Result
```

The table below shows where time is spent on each of the example programs
shipped with the package:

```{r pipeline-table, echo = FALSE, results = "asis"}
if (has_bench_data) {
  real_names <- c("fibonacci.arl", "quicksort.arl",
                  "graph-paths.arl", "macro-examples.arl")
  components <- c("tokenizer", "parser", "macro", "compile", "r-eval")
  comp_labels <- c("Tokenizer", "Parser", "Macro Expander", "Compiler", "R eval()")

  comp_vals <- sapply(components, function(comp) {
    vapply(real_names, function(wl) {
      idx <- which(benches$name == paste0(comp, "/real/", wl))
      if (length(idx) == 1) benches$value[idx] else NA_real_
    }, numeric(1))
  })

  totals <- rowSums(comp_vals, na.rm = TRUE)
  pct_mat <- 100 * comp_vals / totals

  fmt_cell <- function(ms, pct) {
    if (is.na(ms)) return("\u2014")
    sprintf("%.0f ms (%.0f%%)", ms, pct)
  }

  rows <- lapply(seq_along(real_names), function(i) {
    cells <- vapply(seq_along(components), function(j) {
      fmt_cell(comp_vals[i, j], pct_mat[i, j])
    }, character(1))
    data.frame(
      Workload = paste0("`", real_names[i], "`"),
      Tokenizer = cells[1],
      Parser = cells[2],
      `Macro Expander` = cells[3],
      Compiler = cells[4],
      `R eval()` = cells[5],
      check.names = FALSE,
      stringsAsFactors = FALSE
    )
  })
  tbl <- do.call(rbind, rows)
  knitr::kable(tbl, format = "pipe")
} else {
  cat("*Benchmark data not available — rebuild with access to the `gh-pages` branch.*\n")
}
```

The compiler, R eval(), and macro expander together dominate runtime.
The tokenizer and parser are fast by comparison.

## End-to-End Timings

The same workloads measured end-to-end:

```{r e2e-table, echo = FALSE, results = "asis"}
if (has_bench_data) {
  e2e_idx <- grepl("^e2e/real/", benches$name)
  e2e <- benches[e2e_idx, , drop = FALSE]
  e2e$workload <- sub("^e2e/real/", "", e2e$name)

  tbl <- data.frame(
    Workload = paste0("`", e2e$workload, "`"),
    `Execution Time` = sprintf("~%.0f ms", e2e$value),
    check.names = FALSE,
    stringsAsFactors = FALSE
  )
  knitr::kable(tbl, format = "pipe")
} else {
  cat("*Benchmark data not available — rebuild with access to the `gh-pages` branch.*\n")
}
```

These are end-to-end times (i.e., tokenize + parse + expand + compile +
evaluate), and also include engine startup and module loading.

## Optimizations Applied

Three O(n²) bottlenecks were identified by code inspection and fixed:

1. **Tokenizer string accumulation** — character-by-character `c()` replaced
   with a regex-based approach that processes entire tokens in one pass,
   eliminating per-character overhead entirely.

2. **Parser list growing** — repeated `c(elements, list(elem))` replaced
   with chunked collection. ~9× improvement for large flat lists
   (1000 elements: ~60 ms → 6.9 ms).

3. **CPS overhead removed** — the evaluator was converted from
   continuation-passing style with trampolines to a compiler that emits
   R code evaluated by R's native `eval()`, removing per-expression
   closure allocation.

```{r before-after, echo = FALSE, results = "asis"}
if (has_bench_data && !is.null(first_benches)) {
  # Compare first run (baseline) to latest run for e2e/real workloads
  e2e_names <- benches$name[grepl("^e2e/real/", benches$name)]
  rows <- lapply(e2e_names, function(nm) {
    before <- first_benches$value[first_benches$name == nm]
    after  <- benches$value[benches$name == nm]
    if (length(before) == 1 && length(after) == 1) {
      data.frame(
        Workload = paste0("`", sub("^e2e/real/", "", nm), "`"),
        Before = sprintf("~%.0f ms", before),
        After = sprintf("~%.0f ms", after),
        Speedup = sprintf("%.1fx", before / after),
        check.names = FALSE,
        stringsAsFactors = FALSE
      )
    }
  })
  tbl <- do.call(rbind, rows)
  if (!is.null(tbl) && nrow(tbl) > 0) {
    cat("\nBefore/after comparison (first recorded run vs latest):\n\n")
    print(knitr::kable(tbl, format = "pipe"))
  }
} else if (!has_bench_data) {
  cat("*Benchmark data not available.*\n")
}
```

All fixes preserve correctness (full test suite passes) and significantly
improve performance on most code, as well as preventing worst-case blowups.

## Benchmark Data

Historical benchmark results are stored on the `gh-pages` branch in
`dev/bench/data.js`. You can inspect them with:

```bash
git show gh-pages:dev/bench/data.js
```

Each entry records a benchmark run with commit metadata and per-benchmark
timings. The tables on this page are generated from the latest run.

## Running Benchmarks

From the repository root:

```bash
make bench
```

This runs the full benchmark suite (component-level and end-to-end) and
saves results to `benchmarks/results/`. Individual component benchmarks
are also available:

```bash
# COMPONENT can be tokenizer, parser, macro, compile, r-eval, stdlib, e2e
make bench-component COMPONENT=tokenizer
```

Profiling reports (HTML flame graphs) can be generated with:

```bash
make profile

# View: open benchmarks/profiles/eval-fibonacci.html
```
