Status: Accepted (deferral)
Two structural costs in the core runtime were measured this cycle. Each has a known fix that is real work with real risk, and neither prize is large enough to justify that work now. This ADR records the measurements and the decision to defer, with the trigger that should reopen each.
The protocol layer (0.14.203) made the common core fns reference their protocol
slot to dispatch to custom types: assoc names IAssociative__assoc, conj
names ICollection__conj, get names ILookup__lookup, = names
IEquiv__equiv, and so on. ADR 0006 and symbol-pure-dce shake out the slots an
app does not use. But a broad app uses assoc and conj and count and get
and = together, so the union of its fns references the whole slot set, plus
INSTANCE_TYPE. That block is then pinned into the bundle.
Measured on the js-framework-benchmark reagami app (same compiled app, only the runtime core.js swapped):
| runtime | raw (min) | gzip |
|---|---|---|
| main | 25973 | 9197 |
| 0.14.202 (pre-protocol) | 24136 | 8659 |
| delta | +1837 | +538 |
reagami and the demo define no deftype/defrecord/extend-type, so for them
the whole block is dead weight: it dispatches to custom impls that do not exist.
This is true of most plain-data apps.
The fix is to invert the dispatch, like CLJS: base fns handle only native types
(Object/Array/primitive/Map/Set) and name no protocol slot; extend-type and
deftype/defrecord install the dispatch. An app with no protocol impls pulls
zero protocol machinery. The DCE-clean mechanism is for extend-type to wrap the
affected core fns, so the base fn holds no protocol reference to retain. The
binding constraint is that ESM imports are read-only, so the fns need a mutable
holder or a registry indirection.
min/max/conj and other variadic core fns take (...xs), allocating a rest
array on every call including the common fixed-arity case. Evidence from the
0.14.199 min regression: a keyed vdom patch calls min once per element,
~7000x per render, and reagami regressed ~18% on the benchmark remove op.
Dispatching on arguments.length inside the rest-args fn does not help - the
check is free, but V8 elides the rest array only when inlining plus escape
analysis both succeed, and in throttled Chrome they did not (a fast-arity runtime
min measured as slow as plain rest-args; only the Math.min intrinsic restored
0.14.197 speed). So the fix has to be at the call site: emit a per-arity call
(CLJS .cljs$core$IFn$_invoke$arity$2 style). #885 tried inline emission and
#887 reverted it (double-evaluation); per-arity functions avoid that. This is the
doc/ai/ideas.md "Emit direct fixed-arity calls" idea.
Defer both. Ship neither fix now.
extend-type. The trade does not clear the bar
today.min/max
arity that node cannot show a win for and that Chrome does not benefit from. The
real fix is the compiler-level per-arity emission, which is larger.bb test:size runs in CI (this ADR's companion change), so the protocol cost
is visible per commit and a regression past the threshold fails the build.min cost with js/Math.min and does not depend on
either fix. It stays current with squint and pays the ~1/2 KB, still the
smallest app in the benchmark table.test:size shows the tax crossing a threshold that
matters, or a size-sensitive consumer is blocked by it - by then the protocol
code is battle-tested and the inversion is far less risky than doing it now
against fresh code.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |