Performance and debugging
Keep your scripts fast and stable: hot-path rules, memory hygiene, and where to look when things go wrong.
The performance model
The cost structure per frame per instance:
| Stage | Cost |
|---|---|
| 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 writes | Bump allocation inside the arenas; zero allocations, zero boxing |
Pixel rasterization (Gfx2D) | Pure C++ loops; src-over fast path for opaque colors |
| Host replay | The same render path as JS; texture uploads move only dirty rectangles |
Api calls | A JSON encode + reflection + JSON decode round trip โ Expensive use only as a low-frequency channel |
Hot-path rules
- Never touch
Apiinside 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". - Create resources only in
create: models, textures, Java object handles, callbacks โ build them once, store them inState, and merely use them inrender. - Avoid per-frame asset reads / HTTP: put networking on
BackgroundWorker, the results into your own synchronized cache, and have render read the cache. - Upload textures only when content changes: paint the static background once; bound the repaint region of dynamic content with
push_clipso the dirty rectangle covers only the strips that actually changed. - 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()orgame_time_millis()as the clock). - Be frugal with string temporaries: comparing
StrViews is copy-free; when you truly must concatenate, mindstd::stringallocations (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 insiderender/disposeare handed to the host's background reclamation thread, avoiding charging the full cost offreeto 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; callhost->defer_freeby 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 at | Where | What you'll see |
|---|---|---|
| Module census | The startup section of logs/latest.log | Loaded 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 reasons | Census lines + debug mode | If either side (JS or C++) is missing / fails to load, only that side is skipped with a note; the other side keeps working |
| Crash guard | latest.log | native script faulted: code=โฆ module disabled โ the precise reason a module was disabled |
| Sweep traffic | latest.log (one line per minute) | An aggregate log of instance-destruction pacing; lets you observe instance lifetime behavior directly |
| Uploaded bytes | GraphicsTexture::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
| Flag | Default | Effect |
|---|---|---|
-Djcm.nativeIdleMillis | 90000 | When 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.nativeIdleMax | 32 | Cap on hot instances; beyond it the least recently rendered are released first; 0 = no cap |
-Djcm.ffm.disable | off | Debug 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?
- 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). - 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).
- Check that
IDmatches the declaration entry'sidcharacter for character. - 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
Apicalls inside render (including indirect ones: a few context methods such asctx.set_auto_z_orderingforward 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.