Skip to content

Add experiments framework for compiler perf work - #13

Merged
kozak merged 7 commits into
restaumaticfrom
experiments
Apr 17, 2026
Merged

Add experiments framework for compiler perf work#13
kozak merged 7 commits into
restaumaticfrom
experiments

Conversation

@kozak

@kozak kozak commented Apr 16, 2026

Copy link
Copy Markdown

Summary

  • Adds experiments/ directory with structured framework for tracking compiler performance optimization experiments
  • Includes measurement harness (run-profile.sh), lifecycle driver (exp script), baseline management, and per-experiment schema (EXPERIMENT.md, TASK.md, HANDOFF.md, results.md)
  • Seeds index with stubs for three existing experiments (tc-queries, synonym-opt, rust-interning) and a completed noise-check validation
  • Adds CLAUDE.md with guidance on performance work, measurement scenarios, and noise discipline
  • Archives legacy profiling artifacts from repo root

Details

The framework provides:

  • Four measurement scenarios (full, nochange, prelude, leaf) with warm-up discard + median reporting
  • Baseline binary management keyed by commit SHA with provenance manifest
  • Cross-experiment lessons (LESSONS.md) to prevent re-attempting known dead ends
  • Noise-check validation: baseline-vs-self measured at +0.1% delta, confirming harness reliability

Test plan

  • Noise check passed: +0.1% delta (71041ms vs 71093ms medians)
  • Full build baseline matches historical ~72s reference
  • purs compile $(spago sources) with set -f correctly bypasses shell glob expansion
  • Run exp run against an existing experiment branch to verify end-to-end flow

🤖 Generated with Claude Code

@kozak
kozak requested a review from zyla April 16, 2026 07:44
kozak and others added 5 commits April 16, 2026 11:26
Instrument the typechecker with traceMarker calls at three levels:

- Per-declaration: wraps each case in typeCheckAll (TypeChecker.hs)
  Format: "tc ModuleName kind:name start/end"

- Per-phase: wraps infer vs constraint-solve in typesOf (Types.hs)
  Format: "tc-phase ModuleName bindName infer|solve start/end"

- Per-constraint: wraps each entails call in the solver (Entailment.hs)
  Format: "tc-entails ClassName start/end" (recursive, shows full call stack)

The markers use traceMarker (pure, zero cost when not profiling).
GHC 9.6 enables eventlog by default — no build flags needed.

Includes analysis scripts:
- debug/eventlog.js: text report with declaration + phase breakdown
- debug/eventlog-speedscope.js: Chrome trace format for flamegraph
- debug/README.md: profiling workflow docs

Initial profiling of pr-admin identified Data.Record.HasField as 81.7%
of entailment time (35s/43s). See experiments/experiment_queue.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Entailment start markers now include abbreviated type arguments:
  "tc-entails Data.Record.HasField 'name' Int Record start"
- After solving, emit instance marker:
  "tc-entails-instance Data.Record.HasField hasFieldRecord"
- Move tracing into recursive solve/go so sub-constraints nest properly
- Converter: attach type args as Chrome trace `args` (visible on click),
  emit instance resolution as instant events
- Add --min-ms flag to converter to skip short entailment events
  (--min-ms 1 reduces profile from 138MB to 28MB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous approach used fmap (\r -> traceMarker endTag r) which
fires the end marker when the result Expr is demanded, not when the
solve action completes. With lazy StateT/WriterT this causes end
markers to fire out of order, making timing data and nesting in
profile.json completely unreliable.

Fix: emit let !_ = traceMarker endTag () before each return in the
three solver branches (Solved, Unsolved, Deferred). The bang pattern
forces the marker in the monadic sequence after the solving work
completes.

Also adds debug/analyze-hasfield.js for detailed HasField entailment
analysis from eventlog data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The entailment solver's fundep enforcement unifies inferred types with
constraint types, but for wide row types (e.g. 667-field Translations
record), both sides are often structurally identical. Adding an eqType
guard before unifyTypes avoids O(n) row alignment on identical types.

Results on pr-admin (1758 modules):
  full:     74.1s -> 62.6s  (-15.5%)
  nochange: 1.24s -> 1.29s  (within noise)
  prelude:  5.25s -> 5.44s  (within noise)
  leaf:     2.03s -> 2.00s  (within noise)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename debug/eventlog-speedscope.js -> debug/eventlog-chrome-trace.js
  to reflect that we primarily use chrome://tracing, not speedscope

- Add `exp profile <id> --phase before|after` command that:
  - Runs an app-only eventlog build of pr-admin (clears Restaumatic.*
    only, keeps deps cached for smaller traces and faster profiling)
  - Converts to chrome trace JSON via eventlog-chrome-trace.js
  - Outputs to experiments/<id>/<id>-profile-{before,after}.json

- Add experiments/.gitignore for profile JSONs (100-150MB)

- Update CLAUDE.md, SCHEMA.md, scripts/README.md, debug/README.md
  with the new workflow and renamed script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kozak
kozak merged commit e012516 into restaumatic Apr 17, 2026
4 of 6 checks passed
@kozak
kozak deleted the experiments branch April 17, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant