Skip to main content

juno-zero-allocation

Use when writing or reviewing a Juno example program (juno-examples) or a juno-api entry (a new native method, handle type or helper) so it runs in the board's fixed arena with zero allocations, or with a minimum number of objects created once at startup. Covers what allocates on a Juno board, API shapes that never allocate (caller-owned buffers, primitive handles, status returns), the startup-allocation pattern for programs that need objects, and how to verify allocation with compiler metrics, JUNO-RISK findings and the GC log. Triggers include write a Juno example, add a juno-api method, avoid allocation, reduce RAM, arena out of memory, JUNO-RISK-001, JUNO-RISK-002.

Source facts

Repository
jabrena/juno
Last source activity
October 21, 2026 at 19:19
Detected SKILL.md language
English
Stars
2
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
juno-zero-allocation
description
Use when writing or reviewing a Juno example program (juno-examples) or a juno-api entry (a new native method, handle type or helper) so it runs in the board's fixed arena with zero allocations, or with a minimum number of objects created once at startup. Covers what allocates on a Juno board, API shapes that never allocate (caller-owned buffers, primitive handles, status returns), the startup-allocation pattern for programs that need objects, and how to verify allocation with compiler metrics, JUNO-RISK findings and the GC log. Triggers include write a Juno example, add a juno-api method, avoid allocation, reduce RAM, arena out of memory, JUNO-RISK-001, JUNO-RISK-002.
# Zero-allocation Juno programs and API entries Juno compiles Java ahead of time for boards with a few KB of RAM. Every run-time allocation comes from one **fixed arena** (8 KiB by default on the UNO R4 WiFi and UNO Q, 1 KiB on the UNO WiFi Rev2), collected by a conservative, non-moving mark-and-sweep GC. When the arena is full even after a collection, the board stops with `[juno-gc] OOM: need …` on the serial port: there is no `OutOfMemoryError` to catch. `juno-api` and `juno-examples` are therefore written in a deliberately different style from `juno-compiler` and `juno-maven-plugin`: primitives, caller-owned arrays and static intrinsics instead of object graphs. Read `juno-site/src/main/resources/content/memory-model.md` and `limitations.md` for the full model. ## Where data lives | Costs arena (allocates at run time) | Free (no allocation) | |---|---| | `new` of a program class, record or built-in exception | Primitives and local variables (stack) | | Every `new T[n]` array, including `values()`-style copies you write | `static` fields (`.bss`) and enum constants (ordinals) | | Capturing lambdas (each `LambdaCreate` with captured values) | String literals (flash) | | `BigInteger`/`BigDecimal`, atomics, `StringBuilder`, task/scope handles | `static final` arrays with an all-constant initializer, only read (flash `.rodata`) | | A `static final` table the compiler cannot keep in flash (`JUNO-RISK-012`) | `System.getenv("LITERAL")` and `LedCanvas.drawText(…, "literal", …)`, folded at compile time | String concatenation, `String.valueOf` and `StringBuilder.toString()` do not use the arena: they use the **runtime-string pool**, rotating 32-byte slots. A runtime string longer than 31 characters overflows its slot, so split long `Serial` lines into several prints. The optimizer can remove some allocations (it inlines small methods and scalar-replaces objects that never leave a method), but treat that as a bonus: write the program so it does not depend on it, and measure. ## Rules for juno-api entries 1. **Shape the class like the existing API**: a `final`, non-instantiable class of `static native` intrinsics, or static methods over primitives. A handle is a zero-cost wrapper around a primitive (as `DigitalOutput` wraps a pin number), never an object graph. The exception is hardware that code under test must be able to fake: `Gpio`, `I2c`, `Mouse`, `LedMatrix`, `Clock`, `BoardModel` and `BoardMemory` are interfaces whose board instances, `Arduino.getGpio()`, `Arduino.getI2c()`, `Arduino.getMouse()`, `Arduino.getLedMatrix()`, `Arduino.getClock()`, `Arduino.getBoardModel()` and `Arduino.getBoardMemory()`, are `hardware.internal.NativeGpio`/`NativeI2c`/`NativeMouse`/ `NativeLedMatrix`/`NativeClock`/`NativeBoardModel`/`NativeBoardMemory` with the `native` intrinsics. A program takes them once, at startup, and passes them in; tests pass `mock(Gpio.class)`/`mock(I2c.class)`/ `mock(Mouse.class)`/`mock(LedMatrix.class)`/`mock(Clock.class)`/`mock(BoardModel.class)`/`mock(BoardMemory.class)`. 2. **Let the caller own every buffer.** Take a `byte[]`/`int[]` plus an offset or length and fill it; never return a new array or a `String` built at run time. `Udp` (`int[5]` endpoint, caller-owned payload) and the bounded `Json` reader are the reference designs. 3. **Return primitives**: a `boolean` for success, an `int` count or status code. Do not throw on the hot path. 4. **Take compile-time text where it is configuration**: a parameter the intrinsic needs as a literal is declared in `IntrinsicRegistry` (`requiresLiteralStringArgument`/`prefersLiteralStringArgument`), so it costs no runtime string. 5. **Wire the intrinsic**: a new `native` method needs entries in `IntrinsicRegistry` and `IntrinsicLowering` (`juno-compiler`), its shim code gated on use (`ShimFeature`), and a `JunoCompilerTest`/`GeneratedAsmToolchainTest` case. The backend never branches on a specific board. 6. **Document the cost** in the Javadoc: "allocation-free", or exactly what it allocates and when. ## Rules for example programs 1. **Allocate once, before the main loop.** Buffers, frames, entity objects and lookup tables are created at startup and kept in fields or locals of `main`; the `while (true)` loop only mutates them. 2. **Nothing in the loop allocates**: no `new`, no array creation, no capturing lambda, no `StringBuilder`, no boxing (`Integer`, collections, `var` over wrappers), no string concatenation unless it is short and the pool rotation is understood. 3. **Constant data goes to flash**: write tables as `static final` arrays with literal initializers (fonts, sprites, maps, notes) and only read them. 4. **Size arrays with compile-time constants**, assign each array local once, and pass a length alongside every array parameter: that keeps bounds checks and `arraylength` resolvable (see `limitations.md`). 5. **Model state with enums and `int`s**, not small objects: an enum is its ordinal and `switch` on it is a table. 6. **Print in pieces**: `Serial.print("x=")` then `Serial.println(x)` rather than `"x=" + x` in a loop. 7. **When objects make the design clearer** (a game's entities, a parser's state), use a fixed pool created at startup and reuse its members (reset fields instead of creating new instances). Prefer `final` classes without subclasses so calls dispatch statically. ```java // Bad: allocates every frame and builds a runtime string each time while (true) { boolean[][] frame = new boolean[8][12]; Serial.println("score=" + score + " lives=" + lives); } // Good: one frame for the whole run, printed in pieces boolean[][] frame = new boolean[8][12]; while (true) { LedCanvas.clear(frame); Serial.print("score="); Serial.println(score); } ``` ## Verify, do not assume 1. **Compiler findings**: `juno:compile`/`juno:verify` print `JUNO-RISK-*` findings. `JUNO-RISK-001` (allocation that can repeat in a loop) and `JUNO-RISK-002` (startup arena estimate above capacity) are the ones this skill targets; `JUNO-RISK-012` explains a table that stayed in RAM. 2. **Metrics**: `java -jar juno.jar compile --main <class> --metrics` reports `Direct allocation calls`; an allocation-free loop body adds none. 3. **At run time**: build with `-Djuno.gcLog=true` and watch `./mvnw -f juno-examples/pom.xml juno:monitor`: each collection prints one line, so an allocation-free loop prints none after startup. `Arduino.getBoardMemory().arenaUsedBytes()` reports usage from inside the program. 4. **Only if the data genuinely needs it**, raise the arena with `-Djuno.Xmx=<size>` (or `<arenaSize>` in the example's pom); it is a capacity, not a fix for a loop that allocates. 5. Run `./mvnw test`, then `juno:verify` for the example, and flash it to the board before calling it done.
View on GitHub