From 5be0979f4e32899f8a9882a1d196867bc74e0773 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 12 Jan 2026 17:29:20 +0100 Subject: [PATCH 1/2] Implement LRU cache for storing hashes to filter Currently, some busy nodes are seeing more than 128 packets before they are being received again. I'm not sure if this would help since I'm not running one of those nodes. I think it might be beneficial though. Opted for uint16_t instead of uint32_t in order to save memory. --- src/helpers/SimpleMeshTables.h | 79 ++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 2f8af52af..7743d6bed 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,18 +8,19 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 +#define HASH_TTL_MS 60000 // 60 seconds - entries older than this are considered expired class SimpleMeshTables : public mesh::MeshTables { uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; - int _next_idx; + uint16_t _last_seen[MAX_PACKET_HASHES]; // timestamp for LRU + TTL, wraps every 65s uint32_t _acks[MAX_PACKET_ACKS]; int _next_ack_idx; uint32_t _direct_dups, _flood_dups; public: - SimpleMeshTables() { + SimpleMeshTables() { memset(_hashes, 0, sizeof(_hashes)); - _next_idx = 0; + memset(_last_seen, 0, sizeof(_last_seen)); memset(_acks, 0, sizeof(_acks)); _next_ack_idx = 0; _direct_dups = _flood_dups = 0; @@ -28,13 +29,26 @@ class SimpleMeshTables : public mesh::MeshTables { #ifdef ESP32 void restoreFrom(File f) { f.read(_hashes, sizeof(_hashes)); - f.read((uint8_t *) &_next_idx, sizeof(_next_idx)); + int dummy_idx; + f.read((uint8_t *) &dummy_idx, sizeof(dummy_idx)); // legacy, ignore f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); + // Treat restored hashes as just seen - give them fresh timestamps + uint16_t now = millis(); + const uint8_t* sp = _hashes; + for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { + // Check if slot has data (not all zeros) + bool empty = true; + for (int j = 0; j < MAX_HASH_SIZE && empty; j++) { + if (sp[j] != 0) empty = false; + } + _last_seen[i] = empty ? 0 : now; + } } void saveTo(File f) { f.write(_hashes, sizeof(_hashes)); - f.write((const uint8_t *) &_next_idx, sizeof(_next_idx)); + int dummy_idx = 0; + f.write((const uint8_t *) &dummy_idx, sizeof(dummy_idx)); // legacy format f.write((const uint8_t *) &_acks[0], sizeof(_acks)); f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); } @@ -45,7 +59,7 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t ack; memcpy(&ack, packet->payload, 4); for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { + if (ack == _acks[i]) { if (packet->isRouteDirect()) { _direct_dups++; // keep some stats } else { @@ -54,29 +68,57 @@ class SimpleMeshTables : public mesh::MeshTables { return true; } } - + _acks[_next_ack_idx] = ack; - _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table + _next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table return false; } + uint16_t now = millis(); uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); + int oldest_idx = 0; + uint16_t oldest_age = 0; + int expired_idx = -1; + const uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; + uint16_t age = (uint16_t)(now - _last_seen[i]); + + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + // Check if expired (last_seen == 0 means never set) + if (_last_seen[i] != 0 && age <= HASH_TTL_MS) { + // Valid match - refresh timestamp (LRU touch) and return true + _last_seen[i] = now; + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; + } + return true; } - return true; + // Expired match - treat as not seen, reuse this slot + expired_idx = i; + break; + } + + // Track oldest entry for LRU eviction + if (age > oldest_age) { + oldest_age = age; + oldest_idx = i; + } + + // Track first expired slot (including never-used slots where last_seen == 0) + if (expired_idx < 0 && (_last_seen[i] == 0 || age > HASH_TTL_MS)) { + expired_idx = i; } } - memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); - _next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table + // Not found - insert into expired slot or evict oldest (LRU) + int insert_idx = (expired_idx >= 0) ? expired_idx : oldest_idx; + memcpy(&_hashes[insert_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); + _last_seen[insert_idx] = now; return false; } @@ -85,7 +127,7 @@ class SimpleMeshTables : public mesh::MeshTables { uint32_t ack; memcpy(&ack, packet->payload, 4); for (int i = 0; i < MAX_PACKET_ACKS; i++) { - if (ack == _acks[i]) { + if (ack == _acks[i]) { _acks[i] = 0; break; } @@ -96,8 +138,9 @@ class SimpleMeshTables : public mesh::MeshTables { uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { memset(sp, 0, MAX_HASH_SIZE); + _last_seen[i] = 0; break; } } From 91e53228c2cb849a55c3d48602b78d771efe9827 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Tue, 13 Jan 2026 13:36:20 +0100 Subject: [PATCH 2/2] Stick to actual timestamps and no timeout eviction --- src/helpers/SimpleMeshTables.h | 45 ++++++++++++---------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 7743d6bed..829f41f84 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -8,11 +8,10 @@ #define MAX_PACKET_HASHES 128 #define MAX_PACKET_ACKS 64 -#define HASH_TTL_MS 60000 // 60 seconds - entries older than this are considered expired class SimpleMeshTables : public mesh::MeshTables { uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; - uint16_t _last_seen[MAX_PACKET_HASHES]; // timestamp for LRU + TTL, wraps every 65s + uint32_t _last_seen[MAX_PACKET_HASHES]; // timestamp for LRU eviction uint32_t _acks[MAX_PACKET_ACKS]; int _next_ack_idx; uint32_t _direct_dups, _flood_dups; @@ -34,7 +33,7 @@ class SimpleMeshTables : public mesh::MeshTables { f.read((uint8_t *) &_acks[0], sizeof(_acks)); f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx)); // Treat restored hashes as just seen - give them fresh timestamps - uint16_t now = millis(); + uint32_t now = millis(); const uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { // Check if slot has data (not all zeros) @@ -74,33 +73,26 @@ class SimpleMeshTables : public mesh::MeshTables { return false; } - uint16_t now = millis(); + uint32_t now = millis(); uint8_t hash[MAX_HASH_SIZE]; packet->calculatePacketHash(hash); int oldest_idx = 0; - uint16_t oldest_age = 0; - int expired_idx = -1; + uint32_t oldest_age = 0; const uint8_t* sp = _hashes; for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) { - uint16_t age = (uint16_t)(now - _last_seen[i]); - - if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) { - // Check if expired (last_seen == 0 means never set) - if (_last_seen[i] != 0 && age <= HASH_TTL_MS) { - // Valid match - refresh timestamp (LRU touch) and return true - _last_seen[i] = now; - if (packet->isRouteDirect()) { - _direct_dups++; // keep some stats - } else { - _flood_dups++; - } - return true; + uint32_t age = now - _last_seen[i]; + + if (memcmp(hash, sp, MAX_HASH_SIZE) == 0 && _last_seen[i] != 0) { + // Match found - refresh timestamp (LRU touch) and return true + _last_seen[i] = now; + if (packet->isRouteDirect()) { + _direct_dups++; // keep some stats + } else { + _flood_dups++; } - // Expired match - treat as not seen, reuse this slot - expired_idx = i; - break; + return true; } // Track oldest entry for LRU eviction @@ -108,15 +100,10 @@ class SimpleMeshTables : public mesh::MeshTables { oldest_age = age; oldest_idx = i; } - - // Track first expired slot (including never-used slots where last_seen == 0) - if (expired_idx < 0 && (_last_seen[i] == 0 || age > HASH_TTL_MS)) { - expired_idx = i; - } } - // Not found - insert into expired slot or evict oldest (LRU) - int insert_idx = (expired_idx >= 0) ? expired_idx : oldest_idx; + // Not found - evict oldest (LRU) + int insert_idx = oldest_idx; memcpy(&_hashes[insert_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE); _last_seen[insert_idx] = now; return false;