LwM2M Scale Mode and Load Testing
Scale mode lets one emulator container register and hold thousands of LwM2M devices (multi-device runs with clientsCount x concurrentClients). It is off by default; with it off the emulator behaves exactly as before.
Enabling scale mode
| Switch | Default | Description |
|---|---|---|
|
off |
Turns scale mode on. Read once at JVM start; changing it needs a restart. Either source set to |
|
mode ON: max(4096, planned devices); mode OFF: CPUs x 4 |
Size of the shared pool that runs every device’s Observe, firmware/software update and reboot work. Overrides both defaults. |
When the mode is ON:
-
The emulator’s internal thread pools run on virtual threads. Per-device work runs on per-device executor views over one shared timer, so an idle device holds about 0 platform threads (measured locally: 0 platform threads per device against 6 to 7 with the mode off).
-
Each device still has its own UDP socket and its own serialization, so a
response_delayornotify_delaysleep blocks only that device. -
The startup log carries
event=emulator-scale-mode virtualThreads=true shadow=… carriers=<n>. If the emulator cannot switch every pool, it refuses to start (event=emulator-scale-mode-refused) rather than run half-virtual. -
Use
-Djdk.virtualThreadScheduler.parallelism=<n>to change the number of carrier threads (default: the container’s CPUs).
When the mode is OFF (default), and for a single device in either mode, behaviour is unchanged.
|
A load run also needs a high file-descriptor limit (one per device, for example |
Per-device credentials
Both options apply to the device’s security mode. They are read when a run is initialised (/api/init) and when a device starts; a change pushed to a running fleet does not alter the identity of devices that already started.
PSK per device
Set lwm2m.psk.per_device=true:
-
The PSK identity of each device is its endpoint name.
-
The key is the first 16 bytes of
HMAC-SHA256(key = template PSK key, data = UTF-8 endpoint name). The template key is the hex-encoded PSK key configured on the connection, and must be at least 16 bytes. -
A server can therefore provision every device from the formula alone.
-
Absent or
false: every device uses the template identity and key, as before.
X.509 from a CA
With the X.509 security mode and lwm2m.x509.ca.keystore set, every device gets its own certificate minted at start (no EST involved).
| Key | Default | Description |
|---|---|---|
|
(unset) |
Path, inside the container, of the CA PKCS#12 file (mount it read-only). Unset: the device uses the keystore-based credential as before. |
|
the store’s only key entry |
Alias of the CA key entry. |
|
|
Validity of each minted leaf, in days. A non-positive number, or one that ends beyond the supported date range, fails device creation. |
|
|
What the device presents: |
|
(unset) |
Path of a file holding the CA keystore password. This is the only password source: |
The minted leaf has CN equal to the device’s endpoint name, an EC P-256 key, a random serial, clientAuth extended key usage and is signed by the CA key. A self-signed CA is not presented as part of the chain, so leaf_and_ca then presents the leaf only. The certificate is kept per endpoint for the run: a reboot or wake keeps it, a new /api/init mints anew. Minting and EST enrollment on the same device are mutually exclusive and fail device creation with a clear message.
Mount the same CA into every container of a fleet: the server then needs one trust entry for all of them.
Transport knobs
| Key | Default | Description |
|---|---|---|
|
|
CoAP acknowledgment timeout in ms. Must be an integer of at least 1; |
|
|
Protocol-stage threads per device. Same validation (at least 1). It keeps its effect in scale mode: it sets how many tasks the device runs in parallel. |
|
|
Receiver threads per device. Same validation (at least 1). |
|
|
Sender threads per device. Same validation (at least 1). |
|
|
Scale mode only: the largest inbound DTLS datagram in bytes. Range 1500 to 16490; any other value fails device creation. Read once per run at |
A DTLS datagram larger than the limit is counted and logged as event=dtls-datagram-over-limit deviceId=… peer=<address:port> sizeAtLeast=<limit+1> limit=<limit> count=<n>: the first one per endpoint start, then every 1,000th. Raise the limit if a handshake flight (for example an X.509 chain) does not fit.
Devices with identical ack_timeout and thread-count values share one transport configuration (at most 16 distinct sets per JVM; further sets are private to the device, logged once as event=network-config-cap-reached).
Behaviour changes
-
WRITE-ATTRIBUTES sent to an instance that the server created (a server-Created instance) is now applied. It was dropped before.
-
Invalid transport knobs (see above) are refused when the device is created instead of being accepted.
Load testing
The repository ships a capacity harness under loadtest/. Its README.md is the reference for the knob table, verdicts and the host launch; it is not repeated here.
-
emulator-capacity.sh: capacity tool. Steps a fleet up (registered devices, threads, memory per device, the point where it breaks) for five arms: scale mode OFF and ON, each with no security, PSK and (ON only) X.509. -
emucap-host.sh: host launcher. Runs all arms under one operations lock on the demo host, pinned off the QA CPUs, and tears down by label. Its window (WINDOW=any|night), give-up and hard-stop times are documented inloadtest/README.md, section "Demo host launch". -
emucap-native-measure.sh: native measure tool. Runs the emulator jar directly (no container, loopback only) in scale mode and measures the live heap per device, with a PASS/FAIL verdict against the slice’s target.
Run the authoritative numbers on the demo host only; local runs are indicative.