Skip to content

Track Hara and gwdb.common uberjar startup performance #423

Description

@hoebat

Summary

Package and benchmark AOT uberjars for the Hara CLI and the real gwdb.common.* backend workload. Hara benefits substantially from AOT. The backend improves moderately, but its typed Postgres hydration still analyzes project source at runtime and prevents a cwd-independent standalone jar.

Reproduction scripts

  • Foundation Base: bin/benchmark-hara-uberjar
  • greenways-ai/v2 backend: backend/bin/benchmark-gwdb-common-uberjar

Both scripts:

  1. derive/build the relevant AOT uberjar;
  2. compare fresh JVM source-classpath and uberjar execution;
  3. default to three trials via TRIALS=3;
  4. validate functional output/API shape;
  5. print averages, speedup, reduction, and artifact path.

Set SKIP_BUILD=1 to rerun only the benchmark against an existing artifact.

Hara benchmark

Workload:

java ... hara.uberjar.main emit js '[(+ 1 2 3)]'

Validation: both paths emit exactly 1 + 2 + 3.

Three fresh-JVM trials on 2026-07-22:

Mode Trials Average
Source classpath 4.96 s, 4.89 s, 4.89 s 4.913 s
AOT uberjar 1.49 s, 1.51 s, 1.48 s 1.493 s

Result: 3.291x faster, a 69.6% startup reduction.

The Hara build discovers the runtime namespace closure for all supported language specs and AOT-compiles 215 namespaces. The resulting executable artifact is approximately 24 MB and supports languages and emit commands without a Foundation checkout at runtime.

gwdb.common benchmark

Canonical workload: the 40 gwdb.common.* namespaces listed in backend/src/gwbuild/gen/spec/spec_runtime.clj. The benchmark filters that scaffold to common namespaces only.

Semantic validation for both modes:

  • 40/40 namespaces loaded;
  • all namespaces present;
  • 922 public vars exposed.

Three fresh-JVM trials:

Mode Namespace-load average Wall-time average
Source classpath 27,219.727 ms 28.030 s
AOT uberjar 19,518.034 ms 20.203 s

Result:

  • namespace loading: 1.395x faster, 28.3% reduction;
  • end-to-end wall time: 1.387x faster, 27.9% reduction;
  • approximately 7.7 seconds saved per cold process.

A separate one-trial script smoke test reproduced the result: 26,948 ms source versus 19,307 ms uberjar (1.396x).

REPL comparison

  • First load of the 40 backend namespaces in an already-running project REPL: approximately 21.08 s.
  • Requiring the same already-loaded namespaces: approximately 0.09 ms.
  • An uberjar helps fresh process startup, but cannot beat keeping a hydrated REPL/JVM alive.

Backend limitation found

The backend uberjar is AOT compiled, but it is not standalone outside the backend project directory.

Launching it from /tmp fails with:

Cannot find project

The path is:

  1. AOT namespace initializers execute defn.pg/typed hydration hooks.
  2. pg-deftype-hydrate-hook calls app/app-rebuild.
  3. module-typed calls postgres.typed.typed-parse/analyze-namespace.
  4. analyze-namespace calls code.project/project and resolves namespace source files from the checkout.

Consequences:

  • compiled classes still perform substantial runtime source analysis;
  • project.clj and backend source paths must be available relative to the process cwd;
  • the runtime analysis/rebuild dominates the remaining ~19.5 seconds.

The uberjar build also reports duplicate resource warnings because backend src appears in both :source-paths and :resource-paths.

Follow-up areas

  • Cache or serialize typed namespace analysis during the build.
  • Hydrate the runtime registry from packaged data instead of reparsing checkout source.
  • Make project/source lookup resource-aware if source fallback remains necessary.
  • Add a dedicated backend :main once the runtime no longer depends on the checkout cwd.
  • Add benchmark regression thresholds after results are stable across CI runners.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions