Performance and debugging
Keeping scripts fast and stable: hot path rules, memory hygiene, and where to look when things go wrong.
Performance model
The cost structure per frame per instance:
| Stage | Cost |
|---|---|
| Snapshot marshalling (host → module) | The only mandatory Java crossing: the host writes the POD snapshot into a shared page / direct buffer with reused builders |
Data reads (Vehicle / Pids / …) | Plain memory reads; zero cost |
| Draw record writes | Bump allocation inside the arena; no heap allocation, no boxing |
Pixel rasterisation (Gfx2D) | Pure C++ loops; a src-over fast path for opaque colours |
| Host replay | The same render path as JS; texture uploads carry only the dirty rectangle |
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 periodic GC stutters of the JS era were caused precisely by 'one reflective call per vehicle per frame'. - Build resources only in
create: models, textures, Java object handles, callbacks—build them, store them inState, and merely use them inrender. - Avoid per-frame resource reads / HTTP: put networking on the
BackgroundWorker, the results into your own synchronised cache, and have render read the cache. - Upload textures only when content changes: paint static backgrounds 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 the marquees / blinking of multiple instances all move in the same frame—use phase-offset scheduling to spread dirty regions across different frames (use
in_game_time()orgame_time_millis()as the clock). - Be frugal with string temporaries:
StrViewcomparisons are copy-free; when you really must concatenate, mind thestd::stringallocations (short strings are allocation-free thanks to SSO).
Memory and deferred reclamation
The host-side memory discipline (script authors need only be aware of it; the SDK benefits automatically):
- The five output arenas are one per module, reused between frames; the pixel arena starts at 1 MB, grows on demand (capped at 16 MB) and deterministically shrinks back to the floor after a spike—no more fixed 16 MB residency per module.
- Instances leaving the view frustum are not destroyed (they stay warm, so re-entering costs zero rebuilds); only after genuinely leaving the client's vehicle list, roughly 5 seconds later, do they retire.
- Route large frees through
defer_free(ABI 10): megabyte-scale buffers that die insiderender/disposeare handed to the host's background reclamation thread, so the full cost offreeis not booked to the current frame and GC safepoint waits are not amplified. The 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 shrinking already complies automatically; callhost->defer_freeby the same rules for your own large buffers—if the host is too old (the slot is null), just free synchronously.
Observability and logs
| What to look at | Where | What it shows |
|---|---|---|
| Module census | The startup section of logs/latest.log | The number and kinds of loaded modules; the transport channel summary line (containing transport:: shared-memory / panama-ffm and so on)—confirm that shm / FFM is actually in effect, or whether it silently degraded |
| Why loading failed | The census line + debug mode | A missing / failed side (JS or C++) is only skipped with a note left behind, while the other side keeps working as normal |
| 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 aggregated log of instance destruction pacing; instance lifetime behaviour is directly observable |
| Uploaded bytes | GraphicsTexture::last_upload_bytes() | Instrument it in your own code: whether the dirty-rectangle strategy is working is plain to see |
After changing a library, press F3+T (or the mod's 'Reload all MTR scripts' keybind) to reload the 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 been rendered for a long time, release it only after this many milliseconds; 0 = never release on a time limit |
-Djcm.nativeIdleMax | 32 | Cap on warm instances; beyond it, the longest-unrendered ones are released first; 0 = no cap |
-Djcm.ffm.disable | Off | Debug switch: disables the Panama FFM loading path |
Instance retirement and destruction additionally has a per-tick budget (2 by default, oldest first) plus a one-off flush when a world unloads / the dimension changes / the player disconnects.
Frequently asked questions
The module does not load at all?
- Confirm you are using a Release or Complete Build jar—fast CI artefacts lack the native bridge, and the log will say
native bridge unavailable(that is not your module's fault). - Check the platform directory: the current system loads only the library for its own platform; extension-less stem declarations expand automatically to per-platform paths (see Packaging).
- Check that
IDand the declaration entry'sidmatch verbatim. - 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) appear on screen?
CJK glyphs degrade when the host font does not cover the code point—pass a resource font id to Text::font() / the Gfx2D constructor (e.g. jsblock:shanghai), or check that the font set you use contains these 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 multiple instances all move in the same frame: stagger the marquees / blinking / door animations per object.
- Finally check for
Apicalls inside render (including indirect ones: a few context methods such asctx.set_auto_z_orderingforward one call on ABI 8+).
Behaviour differs on Windows?
Historical versions before ABI 6 relied on 'zero-filled memory being usable directly as an object'—which happened to survive only under the MSVC layout. The current SDK's mtrInit() contract has cured this for good; if you are writing a custom host, always call mtrInit() after allocating the state block.
Back to: documentation home.