Performance

March 20, 2026 · View on GitHub

Dataset: 28,014 packets, ~650 nodes, 2 observers
Hardware: ARM64 (MikroTik CCR2116), single-core Node.js

A/B Benchmark: v2.0.1 (before) vs v2.1.0 (after)

All times are averages over 3 runs. "Cached" = warm TTL cache hit.

Endpointv2.0.1v2.1.0 (cold)v2.1.0 (cached)Speedup
Bulk Health7,059 ms3 ms1 ms7,059×
Node Analytics381 ms2 ms1 ms381×
Hash Sizes353 ms193 ms1 ms353×
Topology685 ms579 ms2 ms342×
RF Analytics253 ms235 ms1 ms253×
Channels206 ms77 ms1 ms206×
Node Health195 ms1 ms1 ms195×
Node Detail133 ms1 ms1 ms133×
Channel Analytics95 ms73 ms2 ms47×
Packets (grouped)76 ms33 ms28 ms
Stats2 ms1 ms1 ms
Nodes List3 ms2 ms2 ms
Observers1 ms8 ms1 ms

Architecture

Two-Layer Performance Stack

  1. In-Memory Packet Store (packet-store.js)

    • All packets loaded from SQLite into RAM on startup (~28K packets = ~12MB)
    • Indexed by id, hash, observer, and node (Map-based O(1) lookup)
    • Ring buffer with configurable max memory (default 1GB, ~2.3M packets)
    • SQLite becomes write-only for packets — reads never touch disk
    • New packets from MQTT written to both RAM + SQLite
  2. TTL Cache (server.js)

    • Computed API responses cached with configurable TTLs (via config.json)
    • Smart invalidation: packet bursts only invalidate channels/observers; analytics expire by TTL only
    • Pre-warmed on startup: subpaths, RF, topology, channels, hash-sizes, bulk-health
    • Result: most API responses served in 1-2ms from cache

Key Optimizations

  • Eliminated all LIKE '%pubkey%' queries: Every node-specific endpoint was doing full-table scans on the packets table via decoded_json LIKE '%pubkey%'. Replaced with O(1) pktStore.byNode Map lookups.
  • Single-pass computations: Channels, analytics, and subpaths computed in one loop instead of multiple SQL queries.
  • Client-side WebSocket prepend: New packets appended to the table without re-fetching the API.
  • RF response compression: Server-side histograms + scatter downsampling (1MB → 15KB).
  • Configurable everything: All TTLs, packet store limits, and thresholds in config.json.

What Didn't Work

  • Background refresh (setInterval): Attempted to re-warm caches at 80% TTL. Blocked the event loop — Node.js is single-threaded. Response times went from 3ms to 1,200ms. Reverted immediately.
  • Worker threads: structuredClone overhead of 416ms for 28K packets negated the compute savings. Only viable at 10× data growth or with SharedArrayBuffer (zero-copy).

Running the Benchmark

# Stop the production server first
supervisorctl stop meshcore-analyzer

# Run A/B benchmark (launches two servers: old v2.0.1 vs current)
./benchmark-ab.sh

# Restart production
supervisorctl start meshcore-analyzer