Performance and debugging

Performance model

The cost structure per frame per instance:

StageCost
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 writesBump allocation inside the arena; no heap allocation, no boxing
Pixel rasterisation (Gfx2D)Pure C++ loops; a src-over fast path for opaque colours
Host replayThe same render path as JS; texture uploads carry only the dirty rectangle
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 periodic GC stutters of the JS era were caused precisely by 'one reflective call per vehicle per frame'.
  2. Build resources only in create: models, textures, Java object handles, callbacks—build them, store them in State, and merely use them in render.
  3. Avoid per-frame resource reads / HTTP: put networking on the BackgroundWorker, the results into your own synchronised cache, and have render read the cache.
  4. Upload textures only when content changes: paint static backgrounds 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 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() or game_time_millis() as the clock).
  6. Be frugal with string temporaries: StrView comparisons are copy-free; when you really must concatenate, mind the std::string allocations (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 inside render / dispose are handed to the host's background reclamation thread, so the full cost of free is 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; call host->defer_free by 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 atWhereWhat it shows
Module censusThe startup section of logs/latest.logThe 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 failedThe census line + debug modeA missing / failed side (JS or C++) is only skipped with a note left behind, while the other side keeps working as normal
Crash guardlatest.lognative script faulted: code=… module disabled—the precise reason a module was disabled
Sweep trafficlatest.log (one line per minute)An aggregated log of instance destruction pacing; instance lifetime behaviour is directly observable
Uploaded bytesGraphicsTexture::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

FlagDefaultEffect
-Djcm.nativeIdleMillis90000When 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.nativeIdleMax32Cap on warm instances; beyond it, the longest-unrendered ones are released first; 0 = no cap
-Djcm.ffm.disableOffDebug 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?

  1. 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).
  2. 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).
  3. Check that ID and the declaration entry's id match verbatim.
  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) 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 Api calls inside render (including indirect ones: a few context methods such as ctx.set_auto_z_ordering forward 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.