Macula Performance Optimization Guide

View Source

Overview

Macula implements several performance optimizations to achieve high-throughput pub/sub messaging over distributed DHT routing. This guide documents the caching, routing, and rate-limiting mechanisms that enable 10,000+ msg/sec throughput.

Key Modules:

  • macula_subscriber_cache - Topic→Subscribers caching with TTL
  • macula_direct_routing - NodeId→Endpoint direct routing table
  • macula_gateway_pubsub_router - Optimized message distribution

Performance Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                         PubSub Message Flow                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Publisher                                                              │
│     │                                                                   │
│     ▼                                                                   │
│  ┌──────────────────┐                                                   │
│  │ macula:publish() │                                                   │
│  └────────┬─────────┘                                                   │
│           │                                                             │
│           ▼                                                             │
│  ┌────────────────────────────────────────────────────────────────┐     │
│  │               Subscriber Cache Layer                            │     │
│  │  ┌─────────────────────────────────────────────────────────┐   │     │
│  │  │ macula_subscriber_cache                                  │   │     │
│  │  │  • ETS-backed O(1) lookup                               │   │     │
│  │  │  • TTL-based expiration (default: 5s)                   │   │     │
│  │  │  • Rate-limiting (default: 2s between DHT queries)      │   │     │
│  │  └─────────────────────────────────────────────────────────┘   │     │
│  └────────┬───────────────────────────────────────────┬───────────┘     │
│           │                                           │                 │
│    Cache Hit                                   Cache Miss               │
│           │                                           │                 │
│           ▼                                           ▼                 │
│  ┌─────────────────┐                     ┌─────────────────────┐       │
│  │ Use Cached      │                     │ Query DHT           │       │
│  │ Subscribers     │                     │ (rate-limited)      │       │
│  └────────┬────────┘                     └──────────┬──────────┘       │
│           │                                         │                   │
│           └──────────────────┬──────────────────────┘                   │
│                              │                                          │
│                              ▼                                          │
│  ┌────────────────────────────────────────────────────────────────┐     │
│  │               Direct Routing Layer                              │     │
│  │  ┌─────────────────────────────────────────────────────────┐   │     │
│  │  │ macula_direct_routing                                    │   │     │
│  │  │  • NodeId → Endpoint ETS cache                          │   │     │
│  │  │  • TTL-based expiration (default: 5m)                   │   │     │
│  │  │  • Bypasses DHT for known endpoints                     │   │     │
│  │  └─────────────────────────────────────────────────────────┘   │     │
│  └────────┬───────────────────────────────────────────┬───────────┘     │
│           │                                           │                 │
│    Route Hit                                   Route Miss               │
│           │                                           │                 │
│           ▼                                           ▼                 │
│  ┌─────────────────┐                     ┌─────────────────────┐       │
│  │ Direct QUIC     │                     │ Use Endpoint from   │       │
│  │ Connection      │                     │ DHT Subscriber Info │       │
│  └────────┬────────┘                     └──────────┬──────────┘       │
│           │                                         │                   │
│           └──────────────────┬──────────────────────┘                   │
│                              │                                          │
│                              ▼                                          │
│                    ┌─────────────────┐                                  │
│                    │ pubsub_route    │                                  │
│                    │ via QUIC        │                                  │
│                    └────────┬────────┘                                  │
│                             │                                           │
│                             ▼                                           │
│                    ┌─────────────────┐                                  │
│                    │   Subscribers   │                                  │
│                    └─────────────────┘                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Optimization 1: Subscriber Cache

Problem

DHT lookups add 50-200ms latency per message. For high-frequency topics (10+ msg/sec), this creates unacceptable delays.

Solution

Cache discovered subscribers with TTL-based expiration.

Module: macula_subscriber_cache

%% API
-export([
    lookup/1,          %% Look up subscribers (returns {ok, List} or {miss, Key})
    store/2,           %% Store subscribers for topic
    invalidate/1,      %% Invalidate on topology change
    should_query_dht/1,%% Rate-limit check
    record_dht_query/1 %% Record query for rate-limiting
]).

Data Flow

                     ┌─────────────────────┐
                     │  lookup(Topic)      │
                     └──────────┬──────────┘
                                │
                     ┌──────────▼──────────┐
                     │   ETS Lookup        │
                     │ O(1) complexity     │
                     └──────────┬──────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
        ┌─────▼─────┐     ┌─────▼─────┐     ┌─────▼─────┐
        │  Found &  │     │  Found &  │     │ Not Found │
        │  Valid    │     │  Expired  │     │           │
        └─────┬─────┘     └─────┬─────┘     └─────┬─────┘
              │                 │                 │
              ▼                 ▼                 ▼
        {ok, Subs}    Delete + {miss}       {miss, Key}

Configuration

ParameterDefaultDescription
ttl_ms5000Cache entry TTL (5 seconds)
min_discovery_interval_ms2000Rate-limit window (2 seconds)

Performance Impact

ScenarioWithout CacheWith CacheImprovement
First publish50-200ms50-200ms-
Repeated publish (same topic)50-200ms<1ms50-200x
High-frequency (10+ msg/sec)BlockedSmoothCritical

Optimization 2: Direct Routing Table

Problem

After discovering a subscriber, we still need their endpoint address. Storing this mapping allows relay-routed connections without repeated DHT lookups.

Solution

Cache NodeId→Endpoint mappings for relay-routed connections.

Module: macula_direct_routing

%% API
-export([
    lookup/1,             %% Look up endpoint for node
    store/2,              %% Store node→endpoint mapping
    store_from_subscriber/1, %% Store from DHT subscriber info
    remove/1,             %% Remove stale entry
    stats/0               %% Get hit/miss statistics
]).

Data Flow

                     ┌─────────────────────┐
                     │  Route to NodeId    │
                     └──────────┬──────────┘
                                │
                     ┌──────────▼──────────┐
                     │ Direct Routing      │
                     │ Table Lookup        │
                     └──────────┬──────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                                   │
        ┌─────▼─────┐                       ┌─────▼─────┐
        │  Hit      │                       │  Miss     │
        │ (cached)  │                       │           │
        └─────┬─────┘                       └─────┬─────┘
              │                                   │
              ▼                                   ▼
    Direct QUIC to                    Use endpoint from
    cached endpoint                   subscriber info

Entry Structure

{NodeId, Endpoint, ExpiresAt}
%% Example:
{<<NodeId:256>>, <<"https://192.168.1.10:4433">>, 1700000000000}

Configuration

ParameterDefaultDescription
ttl_ms300000Route entry TTL (5 minutes)
cleanup_interval_ms60000Cleanup frequency (1 minute)

Performance Impact

ScenarioWithout Direct RoutingWith Direct RoutingImprovement
Known subscriber10-50ms<1ms10-50x
Second message to same nodeFull lookupDirectSignificant

Optimization 3: Rate-Limited DHT Discovery

Problem

When cache expires during high-frequency publishing, multiple publishes trigger simultaneous DHT queries ("discovery storms").

Solution

Allow only one DHT query per topic within a minimum interval.

Algorithm

┌─────────────────────────────────────────────────────────────────┐
│                    Rate-Limiting Flow                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────┐                                           │
│  │ Cache Miss       │                                           │
│  │ (need DHT query) │                                           │
│  └────────┬─────────┘                                           │
│           │                                                     │
│           ▼                                                     │
│  ┌────────────────────────────────────────────────────────┐     │
│  │ should_query_dht(Topic)                                │     │
│  │                                                        │     │
│  │  Now = current_time()                                  │     │
│  │  LastQuery = rate_limit_table[Topic]                   │     │
│  │                                                        │     │
│  │  if (Now - LastQuery) >= min_discovery_interval:       │     │
│  │      return true   ──────────────────────────┐         │     │
│  │  else:                                       │         │     │
│  │      return false  ──────────────┐           │         │     │
│  │                                  │           │         │     │
│  └──────────────────────────────────┼───────────┼─────────┘     │
│                                     │           │               │
│                           ┌─────────▼───┐  ┌────▼────────┐      │
│                           │ Rate-limited│  │Query DHT    │      │
│                           │ Skip query  │  │Store result │      │
│                           │ Use best-   │  │in cache     │      │
│                           │ effort      │  │             │      │
│                           └─────────────┘  └─────────────┘      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Configuration

ParameterDefaultDescription
min_discovery_interval_ms2000Minimum time between DHT queries per topic

Performance Impact

ScenarioWithout Rate-LimitingWith Rate-LimitingImprovement
100 publishes in 1 second (cache expired)100 DHT queries1 DHT query100x reduction
DHT load during traffic burstHighControlledCritical

Combined Performance Results

Benchmark: High-Frequency PubSub

Test Setup:

  • 1 Publisher
  • 3 Subscribers across mesh
  • 100ms publish interval (10 msg/sec)

Results:

ConfigurationLatency (p50)Latency (p99)DHT Queries/sec
No optimizations150ms350ms10.0
+ Subscriber Cache2ms15ms0.2
+ Direct Routing1ms5ms0.2
+ Rate Limiting1ms5ms0.05

Memory Usage

CacheSize per EntryMax EntriesMax Memory
Subscriber Cache~1KB1000 topics~1MB
Direct Routing~100B10000 nodes~1MB
Rate Limit Table~50B1000 topics~50KB

Total overhead: ~2.1MB (bounded, with automatic cleanup)


Tuning Guide

Low-Latency Configuration (Games, Real-time)

%% Shorter TTLs for fresher data
{macula_subscriber_cache, #{
    ttl_ms => 2000,                    %% 2 seconds
    min_discovery_interval_ms => 1000  %% 1 second
}},
{macula_direct_routing, #{
    ttl_ms => 60000                    %% 1 minute
}}

High-Throughput Configuration (IoT, Sensors)

%% Longer TTLs for reduced DHT load
{macula_subscriber_cache, #{
    ttl_ms => 30000,                   %% 30 seconds
    min_discovery_interval_ms => 10000 %% 10 seconds
}},
{macula_direct_routing, #{
    ttl_ms => 600000                   %% 10 minutes
}}

Dynamic Topology Configuration (Nodes join/leave frequently)

%% Balance freshness and performance
{macula_subscriber_cache, #{
    ttl_ms => 5000,                    %% 5 seconds (default)
    min_discovery_interval_ms => 2000  %% 2 seconds (default)
}},
{macula_direct_routing, #{
    ttl_ms => 120000                   %% 2 minutes
}}

Monitoring

Cache Statistics

%% Get subscriber cache stats
Stats = macula_subscriber_cache:stats().
%% Returns:
%% #{
%%   hits => 1234,
%%   misses => 56,
%%   hit_rate => 95.6,
%%   table_size => 42,
%%   rate_limited => 100
%% }

%% Get direct routing stats
RouteStats = macula_direct_routing:stats().
%% Returns:
%% #{
%%   hits => 5678,
%%   misses => 12,
%%   stores => 90,
%%   table_size => 15
%% }

Key Metrics to Monitor

MetricTargetAction if Below Target
Cache Hit Rate>90%Increase ttl_ms
Rate Limited CountStableNormal behavior
Table SizeBoundedCheck for leaks

Implementation Details

Thread Safety

Both caches use ETS with {read_concurrency, true} for lock-free reads:

Table = ets:new(?TABLE, [
    named_table,
    set,
    public,                    %% Direct reads from any process
    {read_concurrency, true}   %% Optimized for concurrent reads
]).

Cleanup Strategy

Automatic periodic cleanup removes expired entries:

handle_info(cleanup, State) ->
    Now = erlang:system_time(millisecond),
    %% Match spec: delete where ExpiresAt < Now
    MatchSpec = [{{'$1', '_', '$2'}, [{'<', '$2', Now}], [true]}],
    ets:select_delete(?TABLE, MatchSpec),
    %% Schedule next cleanup
    erlang:send_after(?CLEANUP_INTERVAL_MS, self(), cleanup),
    {noreply, State}.

Future Optimizations (Planned)

  1. Adaptive TTL - Adjust cache TTL based on topic publish frequency
  2. Predictive Prefetch - Pre-warm cache for topics with predictable patterns
  3. Bloom Filter - Fast negative lookup for non-existent topics
  4. Connection Pooling - Keep QUIC streams warm for hot paths

Last Updated: 2025-11-26 Macula Version: 0.10.1 Status: ✅ Production-ready