Performance and debugging

The performance model

The cost structure per frame per instance:

StageCost
Snapshot marshaling (host โ†’ module)The only mandatory Java crossing: the host writes the POD snapshot into shared pages / direct buffers with reused builders
Data reads (Vehicle / Pids / โ€ฆ)Plain memory reads; zero cost
Draw record writesBump allocation inside the arenas; zero allocations, zero boxing
Pixel rasterization (Gfx2D)Pure C++ loops; src-over fast path for opaque colors
Host replayThe same render path as JS; texture uploads move only dirty rectangles
Api callsA JSON encode + reflection + JSON decode round trip โ€” Expensive use only as a low-frequency channel

Hot-path rules

  1. Never touch Api inside render. High-frequency data is all in the snapshot; high-frequency drawing all goes through the record stream. The JS-era periodic GC stutter was caused precisely by "one reflective call per frame per vehicle".
  2. Create resources only in create: models, textures, Java object handles, callbacks โ€” build them once, store them in State, and merely use them in render.
  3. Avoid per-frame asset reads / HTTP: put networking on BackgroundWorker, the results into your own synchronized cache, and have render read the cache.
  4. Upload textures only when content changes: paint the static background once; bound the repaint region of dynamic content with push_clip so the dirty rectangle covers only the strips that actually changed.
  5. Stagger per object: don't let multiple instances cycle / blink in lockstep on the same frame โ€” use phase-offset scheduling to spread the dirty regions across different frames (in_game_time() or game_time_millis() as the clock).
  6. Be frugal with string temporaries: comparing StrViews is copy-free; when you truly must concatenate, mind std::string allocations (short strings are allocation-free thanks to SSO).

Memory and deferred reclamation

The host side's memory discipline (good for script authors to know; the SDK benefits automatically):

  • The five output arenas are one set per module, reused across frames; the pixel arena starts at 1 MB, grows on demand (capped at 16 MB), and deterministically shrinks back to the floor after spikes โ€” no more fixed 16 MB per-module residency.
  • Instances leaving the view frustum are not destroyed (they stay hot; re-entering costs zero rebuilds); only after truly leaving the client's vehicle list do they retire, about 5 seconds later.
  • Large frees go through defer_free (ABI 10): megabyte-scale buffers that die inside render / dispose are handed to the host's background reclamation thread, avoiding charging the full cost of free to the current frame and amplifying GC safepoint waits. Contract: the block must already be dead, have no destructor, and never be touched again by any thread (host replay included). The SDK's pixel arena shrink already complies automatically; call host->defer_free by the same rules for your own large buffers โ€” if the host is too old (the slot is null), a synchronous free is fine.

Observability and logs

What to look atWhereWhat you'll see
Module censusThe startup section of logs/latest.logLoaded module count and types; a transport-channel summary line (containing transport:: shared-memory / panama-ffm, etc.) โ€” confirms shm / FFM actually engaged, or silently degraded
Load failure reasonsCensus lines + debug modeIf either side (JS or C++) is missing / fails to load, only that side is skipped with a note; the other side keeps working
Crash guardlatest.lognative script faulted: code=โ€ฆ module disabled โ€” the precise reason a module was disabled
Sweep trafficlatest.log (one line per minute)An aggregate log of instance-destruction pacing; lets you observe instance lifetime behavior directly
Uploaded bytesGraphicsTexture::last_upload_bytes()Instrument it in your code: see at a glance whether your dirty-rectangle strategy works

After changing libraries, press F3+T (or the mod's "Reload all MTR scripts" keybind) to reload C++ scripts โ€” unload and rediscovery go through the full path, no game restart needed.

JVM flags

FlagDefaultEffect
-Djcm.nativeIdleMillis90000When an instance still exists but has not rendered for a long time, it is released only after this many milliseconds; 0 = no time-based release
-Djcm.nativeIdleMax32Cap on hot instances; beyond it the least recently rendered are released first; 0 = no cap
-Djcm.ffm.disableoffDebug switch: disables the Panama FFM loading path

Instance retirement and destruction is further paced by a per-tick budget (2 by default, oldest first) plus a one-shot flush on world unload / dimension change / disconnect.

FAQ

Module doesn't load at all?

  1. Confirm you are using a Release or Complete Build jar โ€” fast CI artifacts don't include the native bridge, and the log will say native bridge unavailable (that is not a problem with your module).
  2. Check the platform directory: the current system only loads the library for its own platform; an extension-less stem declaration expands automatically into per-platform paths (see Packaging).
  3. Check that ID matches the declaration entry's id character for character.
  4. Check the ABI: the host accepts 6โ€“11; ABI 8+ libraries need a 2.3.0-beta-2.yanyang.7 or newer host.

Placeholder boxes (tofu) on the screen?

CJK glyphs degrade when the host font doesn't cover the code point โ€” pass a resource-pack font id to Text::font() / the Gfx2D constructor (e.g. jsblock:shanghai), or check whether the font set you use includes those characters.

Occasional frame drops?

  • First check whether you are "repainting the whole screen every frame": paint static content once, repaint dynamic content in clipped strips, and verify with last_upload_bytes().
  • Then check whether many instances move in lockstep on the same frame: stagger the cycling / blinking / door animations per object.
  • Finally, look for Api calls inside render (including indirect ones: a few context methods such as ctx.set_auto_z_ordering forward one call on ABI 8+).

Different behavior on Windows?

Historical versions before ABI 6 relied on "zero-filled memory being usable directly as an object" โ€” which happened to survive only under MSVC layouts. The current SDK's mtrInit() contract has fixed this for good; if you are writing a custom host, make sure to call mtrInit() after allocating the state block.


Back to: Docs home.