Skip to content
Open
Show file tree
Hide file tree
Changes from 145 commits
Commits
Show all changes
172 commits
Select commit Hold shift + click to select a range
d9ae86a
perf(shmem): replace O(n) memmem() scan in addstr() with O(1) hash table
DL6ER Mar 16, 2026
ba3fc20
findQueryID: O(1) direct-mapped cache for dnsmasq ID -> query index
DL6ER Mar 16, 2026
f020963
GC: eliminate O(n) queries memmove via deferred compaction
DL6ER Mar 16, 2026
7b7bad2
perf(gc): eliminate O(queries) scan in recycle() via incremental refc…
DL6ER Mar 16, 2026
1ea4c79
perf(db): replace heap-allocated asprintf with stack buffer in DB wri…
DL6ER Mar 16, 2026
38bb955
perf(gc): batch lookup table removals into single compaction pass
DL6ER Mar 16, 2026
eb1c719
perf(gc): single-pass lookup table recycle+compact in GC
DL6ER Mar 16, 2026
638658e
Regex: check query-type filter before running regexec
DL6ER Mar 16, 2026
3a9559c
Defer ABP pattern generation until after gravity exact-match
DL6ER Mar 16, 2026
d2edd03
EDNS0: move cookie processing into debug-only block
DL6ER Mar 16, 2026
fb2629d
Remove dead gettimeofday call and duplicate DNSSEC check
DL6ER Mar 16, 2026
8f8fd45
API: heap-based top-K selection for domains and clients
DL6ER Mar 16, 2026
ecdd530
Minor hot-path micro-optimizations
DL6ER Mar 16, 2026
ec122fe
Add related CI tests
DL6ER Mar 16, 2026
58d7a53
perf: batch dedup linking table INSERTs + remove sqlite3_clear_bindings
DL6ER Mar 18, 2026
e0926fc
perf: use in_database bool flag instead of generation counter
DL6ER Mar 18, 2026
e90bed8
perf: cache linking table row IDs, eliminate 3 REPLACE subqueries
DL6ER Mar 18, 2026
044135f
perf: cache addinfo row IDs, eliminate last REPLACE subquery
DL6ER Mar 18, 2026
0eaec20
perf: skip antigravity check when no allow-adlists exist
DL6ER Mar 18, 2026
b18378e
perf: split queries_to_database() into three phases to reduce SHM loc…
DL6ER Mar 18, 2026
d4a0646
perf: move find_mac() outside SHM lock to avoid kernel I/O under mutex
DL6ER Mar 18, 2026
1d495d3
perf: cache resolved interface in _FTL_iface() to skip repeated loops
DL6ER Mar 18, 2026
727532a
perf(gravity): skip exact allowlist/denylist SQLite lookups when list…
DL6ER Mar 18, 2026
eb88067
perf(gravity): replace cJSON heap allocations with stack-based ABP pa…
DL6ER Mar 18, 2026
54cdf40
Reduce per-query overhead: fused strcpy_tolower, conditional clock_ge…
DL6ER Mar 18, 2026
6594635
Reduce branching and buffer waste in hot query path
DL6ER Mar 18, 2026
909f346
fix: assign stmt in in_gravity() after lazy initialization
DL6ER Mar 18, 2026
88eff62
fix: use !blockDomain instead of blockDomain == NOT_FOUND
DL6ER Mar 18, 2026
5ef1557
perf: skip time() call in gravityDB_client_check_again() for mature c…
DL6ER Mar 18, 2026
3c4a1fd
perf: cache suffix lengths in is_pihole_domain() to short-circuit str…
DL6ER Mar 18, 2026
5fde06a
Eliminate redundant copies and use strcmp for pre-lowered domains
DL6ER Mar 18, 2026
0c86f1c
perf: remove two unused gettimeofday() calls
DL6ER Mar 18, 2026
15dfd7f
perf: replace strlen(interface) with direct char checks
DL6ER Mar 18, 2026
d9d4d40
perf: replace strlen() with O(1) char checks in ESNI domain test
DL6ER Mar 18, 2026
bf956ab
perf: hoist per-client regex row pointer out of match_regex() loop
DL6ER Mar 18, 2026
811af23
perf: replace time(NULL) in upstream-blocked debug log with query->ti…
DL6ER Mar 18, 2026
5475569
perf: pass domain string into FTL_check_blocking() to skip getstr()
DL6ER Mar 18, 2026
9dfdc88
perf: replace snprintf with memcpy in ABP pattern loop
DL6ER Mar 18, 2026
ac52a73
fix: pass OPT RR length to FTL_parse_pseudoheaders in UDP/TCP receive…
DL6ER Mar 18, 2026
fd42aa2
fix: correct EDE info-code byte-order read in FTL_parse_pseudoheaders
DL6ER Mar 18, 2026
10878ca
perf: eliminate redundant strcpy_tolower in FTL_check_blocking
DL6ER Mar 19, 2026
43e009f
perf: eliminate all redundant SHM lookups in FTL_check_blocking
DL6ER Mar 19, 2026
8e9fb7c
perf: pre-compute ABP suffix lengths in abp_patterns struct
DL6ER Mar 19, 2026
02872d1
perf: replace time(NULL) with query->timestamp in _query_set_status
DL6ER Mar 19, 2026
151be9f
docs: correct stale SELECT EXISTS() comment in gravityDB_prepare_clie…
DL6ER Mar 19, 2026
ecbfa47
perf: set synchronous=NORMAL on WAL-mode disk database
DL6ER Mar 19, 2026
6e31b8f
fix: correct swapped bind parameters in db_update_disk_counter and SQ…
DL6ER Mar 19, 2026
b8a3b0b
fix: use sqlite3_bind_int64 for time_t fields in insert_netDB_device
DL6ER Mar 19, 2026
7df6463
fix: close db on all error paths and use int64 for time_t columns
DL6ER Mar 19, 2026
445e9cd
fix: free escaped_status unconditionally in format_gravity_restored_m…
DL6ER Mar 19, 2026
dd0571c
fix: finalize stmt before returning DB_FAILED on step error in db_que…
DL6ER Mar 19, 2026
766ea74
fix: close db on error paths in flush_network_table
DL6ER Mar 19, 2026
62b154f
fix: defer sqlite3_reset() until after hwaddr use in unify_hwaddr()
DL6ER Mar 19, 2026
c478309
fix: close db and return proper error on prepare failure in api_histo…
DL6ER Mar 19, 2026
ff9a38f
fix: close db on prepare failures in api_history_database_clients()
DL6ER Mar 19, 2026
cfbb297
fix: use sqlite3_bind_double() for from/until in api_history_database…
DL6ER Mar 19, 2026
5cffaf4
fix: use sqlite3_column_int64/time_t for timeslot timestamps (Y2038)
DL6ER Mar 19, 2026
7fc1400
fix: close db on prepare failure in api_stats_database_upstreams()
DL6ER Mar 19, 2026
ae97b50
fix: close db on networkTable_deleteDevice() failure in api/network.c
DL6ER Mar 19, 2026
0a0c495
fix: use sqlite3_column_int64() for lastQuery in api/network.c (Y2038)
DL6ER Mar 19, 2026
5d04034
fix: use sqlite3_column_int64() for lastQuery in api/network.c (Y2038)
DL6ER Mar 19, 2026
dee813c
fix: return DB_FAILED on bind error in db_query_int_from_until[_type]()
DL6ER Mar 19, 2026
89ce3c3
perf: prepare query-types statement once outside the loop
DL6ER Mar 19, 2026
ff7cb71
fix: rollback transaction on early return in parse_neighbor_cache()
DL6ER Mar 19, 2026
1b8ae6f
fix: rollback transaction on all early returns in parse_neighbor_cache()
DL6ER Mar 19, 2026
a73c0c0
fix: add missing ROLLBACK before early returns inside open transactions
DL6ER Mar 19, 2026
1d71531
fix: return false after ROLLBACK in gravityDB_delFromTable() failure …
DL6ER Mar 19, 2026
a93c2bb
fix: abort on bind error in db_query_int_int() and db_query_int_str()
DL6ER Mar 19, 2026
9d43d46
fix: free querystr on like_name allocation failure in gravityDB_readT…
DL6ER Mar 19, 2026
69d0189
fix: only record last_insert_rowid on successful INSERT in _add_messa…
DL6ER Mar 19, 2026
4ceacab
fix: check sqlite3_bind_int() return value in delete_message()
DL6ER Mar 19, 2026
d5ba5ad
fix: correct cacheID sentinel check in _query_set_status()
DL6ER Mar 19, 2026
aadb96a
fix: propagate LIST_NOT_AVAILABLE from ABP gravity pattern loop
DL6ER Mar 19, 2026
de8cdc5
fix: correct upstreamID sentinel checks from > 0 to > -1
DL6ER Mar 19, 2026
7cd0fd9
fix: include query ID 0 in recent-blocked reverse scans
DL6ER Mar 19, 2026
0a368bf
fix: return success instead of true in update_netDB_interface()
DL6ER Mar 19, 2026
881b26a
fix: add_local_interfaces_to_network_table() returned SQLITE_ERROR in…
DL6ER Mar 19, 2026
d3a2526
fix: unlock SHM before continue when getClient() returns NULL in pars…
DL6ER Mar 19, 2026
49f3266
fix: log correct old_status in _query_set_status() debug message
DL6ER Mar 19, 2026
cebad70
fix: use & 0xFF instead of % 0xFF to extract EDNS VERSION byte
DL6ER Mar 19, 2026
7582b64
fix: resolve_this_name() disables IPv6 resolution when resolveIPv4=false
DL6ER Mar 19, 2026
fbc922f
fix: calloc in new_sqlite3_stmt_vec() allocates n² items instead of n
DL6ER Mar 19, 2026
68a19ba
fix: zombie detection in process_alive() never matches
DL6ER Mar 19, 2026
67391c1
fix: session cookie always uses default timeout instead of configured…
DL6ER Mar 19, 2026
0ebae9a
fix: zero-initialise vendor buffer in updateMACVendorRecords()
DL6ER Mar 19, 2026
d549f2a
fix: guard freeaddrinfo() calls in resolve_regex_cnames() on getaddri…
DL6ER Mar 19, 2026
b1e2ac6
fix: add missing space in SQL JOIN clause in api_stats_database_top_i…
DL6ER Mar 19, 2026
a47fd50
fix: correct wait-for argc guard from 5 to 6
DL6ER Mar 19, 2026
9fe755f
fix: prevent OOB write and missing NUL in add_to_fifo_buffer()
DL6ER Mar 19, 2026
e268566
fix: return VERIFY_ERROR when sha256sum() fails in verify_FTL()
DL6ER Mar 19, 2026
98251fc
fix: null-check aliasclient pointer in change_clientcount()
DL6ER Mar 19, 2026
72bfeb6
fix: sort top upstreams by query count, not response count
DL6ER Mar 19, 2026
ff377fc
fix: correct inet_pton assignment precedence in validate_cidr()
DL6ER Mar 19, 2026
414a30f
fix: replace strncat with snprintf in validate_regex_array() error path
DL6ER Mar 19, 2026
d8bdc27
fix: null-check getClient() result in recompute_aliasclient()
DL6ER Mar 19, 2026
2f51202
fix: free getline buffer when target line not found in get_dnsmasq_li…
DL6ER Mar 19, 2026
863ee09
fix: use AF_INET6 instead of AF_INET when parsing REPLY_ADDR6 in lega…
DL6ER Mar 19, 2026
05137fc
fix: guard against NULL current_user in set_config_from_CLI() permiss…
DL6ER Mar 19, 2026
8dd2999
fix: free addrDB before early return on invalid CIDR in subnet_match()
DL6ER Mar 19, 2026
ee09e2d
fix: prevent double sqlite3_finalize() on bind failure in _add_message()
DL6ER Mar 20, 2026
a8dc9f8
fix: use escaped strings in format_connection_error() HTML output
DL6ER Mar 20, 2026
0039eb2
fix: rollback transaction and reset statement on bind failure in expo…
DL6ER Mar 20, 2026
191b1db
fix: guard against out-of-bounds device[] access in process_arp_reply()
DL6ER Mar 20, 2026
dc73466
fix: free path buffer in rotation-file cleanup loop in teleporter import
DL6ER Mar 20, 2026
252836b
fix: free json_files cJSON array on ZIP teleporter import error path
DL6ER Mar 20, 2026
250c107
fix: free punycode buffer when domain validation fails in list API
DL6ER Mar 20, 2026
592152b
fix: close sqlite3 handle on early return in teleporter database vali…
DL6ER Mar 20, 2026
31296f5
fix: free top_blocked and top_clients cJSON objects in api_padd()
DL6ER Mar 20, 2026
61f36f1
fix: close local db handle instead of global gravity_db in gravity_up…
DL6ER Mar 20, 2026
9c5da7f
Minor test suite improvement
DL6ER Mar 20, 2026
043326c
gravity-db: use mmap I/O to eliminate pread() overhead for blocklist …
DL6ER Mar 21, 2026
46afd53
perf: add debug.performance flag for measuring gravity.db and FTL cac…
DL6ER Mar 21, 2026
1113fff
Remove stray newline character
DL6ER Mar 21, 2026
79ae962
Merge branch 'development' into tweak/performance
DL6ER Mar 21, 2026
38f7322
perf: pre-warm gravity.db page cache on startup with posix_fadvise
DL6ER Mar 21, 2026
52c7c49
perf + mem: reorder queriesData and clientsData for memory savings an…
DL6ER Mar 21, 2026
67fa7db
perf: replace locale tolower() with branchless ASCII bit-trick in str…
DL6ER Mar 21, 2026
d485061
perf: defer double_time() past pi.hole early exit in FTL_reply
DL6ER Mar 21, 2026
70ce302
perf: instrument FTL_new_query() and FTL_reply() hot-path components
DL6ER Mar 21, 2026
bf196c8
perf + mem: shrink DNSCacheData from 48 to 36 bytes on all architectures
DL6ER Mar 21, 2026
6b7e0c8
docs: correct and expand alignment/cache comments in datastructure.h
DL6ER Mar 21, 2026
ddc02b7
fix: correct uint64 underflow in performance timing macros
DL6ER Mar 21, 2026
002f72b
perf: replace Jenkins' One-at-a-Time hash with FNV-1a in hashStr()
DL6ER Mar 21, 2026
ef28a61
perf: enable mmap I/O for the on-disk query database
DL6ER Mar 22, 2026
4edba81
fix: add missing gravity/antigravity domain indexes to test schema
DL6ER Mar 22, 2026
7944b66
perf: use shared gravity statements with carray() binding
DL6ER Mar 22, 2026
0d33119
refactor: store client group IDs as int arrays in shared memory
DL6ER Mar 22, 2026
aa5439b
perf: convert all group-filtered queries to shared carray() statements
DL6ER Mar 22, 2026
e3a2128
cleanup: remove unused sqlite3_stmt_vec infrastructure
DL6ER Mar 22, 2026
503fddc
refactor: store adlist IDs in SHM intarray alongside group IDs
DL6ER Mar 22, 2026
5cd86f7
perf: add ANTIGRAVITY_TABLE support, dedup regex queries, and add jun…
DL6ER Mar 24, 2026
5008c11
fix: revert adlist ID caching, query gravity views live for correctness
DL6ER Mar 24, 2026
abc8f7e
perf: skip redundant carray rebinds, use WITHOUT ROWID on junction ta…
DL6ER Mar 24, 2026
263808b
fix: re-fetch SHM pointers after find_mac() unlock/relock cycle
DL6ER Mar 24, 2026
6445513
fix: add missing comma in index_creation array in query-table.h
DL6ER Mar 24, 2026
c10a432
fix: sort database top_domains/top_clients by count and push LIMIT in…
DL6ER Mar 24, 2026
0892ff0
perf: replace correlated subqueries with LEFT JOINs in queries VIEW (…
DL6ER Mar 24, 2026
1e52321
fix: correct "NOL NULL" typo to "NOT NULL" on client.ip in gravity sc…
DL6ER Mar 24, 2026
f039bff
perf: use PK lookup in update triggers instead of UNIQUE text column
DL6ER Mar 24, 2026
dab1001
fix: add missing NULL check after calloc in gravityDB_readTable()
DL6ER Mar 24, 2026
dbe385b
fix: guard against NULL from strtok_r in compile_regex()
DL6ER Mar 24, 2026
72c756b
fix: handle realloc failure in teleporter file upload handler
DL6ER Mar 24, 2026
1da2c16
fix: use alternate signal stack and reset-on-entry for crash handlers
DL6ER Mar 24, 2026
b7a71e3
fix: make SIGTERM and RT signal handlers async-signal-safe
DL6ER Mar 24, 2026
f9cb973
style: format crash backtraces in GDB-style output
DL6ER Mar 24, 2026
511d5cf
fix: zero-initialize hwaddr buffer before find_mac() call
DL6ER Mar 25, 2026
81966b6
fix: add missing ROLLBACK on calloc failure in parse_neighbor_cache()
DL6ER Mar 25, 2026
063c7aa
fix: add missing ROLLBACK in replace_queries_view_with_joins()
DL6ER Mar 25, 2026
46fef48
fix: prevent NULL deref in get_top_domains/get_top_clients when count<=0
DL6ER Mar 25, 2026
9370f29
fix: correct SHM_TIME_EPOCH to match 2026-01-01 00:00:00 UTC
DL6ER Mar 25, 2026
078ce69
fix: skip performance stats dump on fresh start, fix duplicate PID log
DL6ER Mar 25, 2026
136cdee
Increate timeout for build job. 15 minutes is sometimes not enough fo…
DL6ER Mar 25, 2026
c83a24d
Merge branch 'development' into tweak/performance
DL6ER Apr 11, 2026
39bbec8
Merge branch 'development' into tweak/performance
DL6ER Apr 11, 2026
d2f040d
Improve test script harness cleanup/preparation steps
DL6ER Apr 11, 2026
861666b
Fix a stale free(punycode) left over from the merge resolution during…
DL6ER Apr 11, 2026
4a4830a
Merge branch 'development' into tweak/performance
DL6ER Apr 12, 2026
1f889cd
Fix duplicated DNSSEC pre-warm query accidentally slipped through in …
DL6ER Apr 12, 2026
f99b052
Mitigate possible race window after Teleporter test
DL6ER Apr 12, 2026
9049c51
Merge branch 'development' into tweak/performance
DL6ER Apr 15, 2026
336f316
perf: cap SQLite rollback journal / WAL at 32 MiB
DL6ER Apr 15, 2026
967fdbe
cleanup: fetch page_count and page_size in a single round trip
DL6ER Apr 15, 2026
fb134df
docs: pin SQLITE_STATIC lifetime contract on carray binding
DL6ER Apr 15, 2026
0ff71fb
perf: open pihole-FTL.db without per-call mutex
DL6ER Apr 15, 2026
a988fb5
perf: extend SQLITE_OPEN_NOMUTEX to other single-threaded opens
DL6ER Apr 15, 2026
0175e53
perf: shrink SQLite per-connection page cache from ~64 MiB to ~16 MiB
DL6ER Apr 16, 2026
5f386d4
perf: split check_blocking perf timing into allowlist and denylist slots
DL6ER Apr 16, 2026
5807edc
perf: attribute check_blocking outliers to caller and sub-engine
DL6ER Apr 16, 2026
c690cbb
perf: time the non-step wrapper work inside in_gravity()
DL6ER Apr 17, 2026
749024d
perf: defer client group re-check reloads to the database thread
DL6ER Apr 17, 2026
e02f95d
perf: move client group re-checks off the DNS hot path to the DB thread
DL6ER Apr 18, 2026
bcf3e7d
Skip aliasclients in the new gravityDB_recheck_clients()
DL6ER Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ do
"debug" ) debug=1;;
"dev" ) dev=1;;
"test" ) test=1;;
"test-fast" ) test=1; export SKIP_PERF_TEST=1;;
"clean-logs" ) clean_logs=1;;
"clang" ) clang=1;;
"ci" ) builddir="cmake_ci/";;
Expand Down
2 changes: 0 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,6 @@ set(sources
signals.h
timers.c
timers.h
vector.c
vector.h
version.c
version.h
)
Expand Down
3 changes: 1 addition & 2 deletions src/api/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,9 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
{
log_debug(DEBUG_API, "API Auth status: OK");

// Ten minutes validity
if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers),
FTL_SET_COOKIE,
auth_data[user_id].sid, config.webserver.session.timeout.d.ui) < 0)
auth_data[user_id].sid, config.webserver.session.timeout.v.ui) < 0)
{
return send_json_error(api, 500, "internal_error", "Internal server error", NULL);
}
Expand Down
3 changes: 3 additions & 0 deletions src/api/docs/content/specs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,8 @@ components:
type: boolean
timing:
type: boolean
performance:
type: boolean
all:
type: boolean
topics:
Expand Down Expand Up @@ -892,6 +894,7 @@ components:
ntp: false
netlink: false
timing: false
performance: false
all: false
took: 0.003
config_one:
Expand Down
6 changes: 5 additions & 1 deletion src/api/docs/content/specs/info.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,12 @@ components:
properties:
gravity:
type: integer
description: Number of collected exact domains on lists
description: Number of collected exact domains on blocking lists
example: 67906
antigravity:
type: integer
description: Number of collected exact domains on allowing lists
example: 0
groups:
type: integer
description: Number of groups
Expand Down
4 changes: 3 additions & 1 deletion src/api/info.c
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ int api_info_database(struct ftl_conn *api)

cJSON *group = JSON_NEW_OBJECT();
JSON_ADD_NUMBER_TO_OBJECT(group, "gid", st.st_gid); // GID
const struct group *gr = getgrgid(st.st_uid);
const struct group *gr = getgrgid(st.st_gid);
if(gr != NULL)
{
JSON_COPY_STR_TO_OBJECT(group, "name", gr->gr_name); // Group name
Expand Down Expand Up @@ -560,6 +560,7 @@ static int get_ftl_obj(struct ftl_conn *api, cJSON *ftl)
// Source from shared objects within lock
lock_shm();
const int db_gravity = counters->database.gravity;
const int db_antigravity = counters->database.antigravity;
const int db_groups = counters->database.groups;
const int db_lists = counters->database.lists;
const int db_clients = counters->database.clients;
Expand Down Expand Up @@ -590,6 +591,7 @@ static int get_ftl_obj(struct ftl_conn *api, cJSON *ftl)
unlock_shm();

JSON_ADD_NUMBER_TO_OBJECT(database, "gravity", db_gravity);
JSON_ADD_NUMBER_TO_OBJECT(database, "antigravity", db_antigravity);
JSON_ADD_NUMBER_TO_OBJECT(database, "groups", db_groups);
JSON_ADD_NUMBER_TO_OBJECT(database, "lists", db_lists);
JSON_ADD_NUMBER_TO_OBJECT(database, "clients", db_clients);
Expand Down
1 change: 1 addition & 0 deletions src/api/list.c
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ static int api_list_write(struct ftl_conn *api,
// which convert to xn--{{{-pla4gpb.com
if(!valid_domain(punycode, strlen(punycode), false))
{
free(punycode);
if(allocated_json)
cJSON_Delete(row.items);
return send_json_error(api, 400, // 400 Bad Request
Expand Down
3 changes: 2 additions & 1 deletion src/api/network.c
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ static int api_network_devices_DELETE(struct ftl_conn *api)
int deleted = 0;
if(!networkTable_deleteDevice(db, device_id, &deleted, &sql_msg))
{
dbclose(&db);
// Add SQL message (may be NULL = not available)
return send_json_error(api, 500,
"database_error",
Expand Down Expand Up @@ -412,7 +413,7 @@ int api_client_suggestions(struct ftl_conn *api)
cJSON *client = JSON_NEW_OBJECT();
JSON_COPY_STR_TO_OBJECT(client, "hwaddr", sqlite3_column_text(stmt, 0));
JSON_COPY_STR_TO_OBJECT(client, "macVendor", sqlite3_column_text(stmt, 1));
JSON_ADD_NUMBER_TO_OBJECT(client, "lastQuery", sqlite3_column_int(stmt, 2));
JSON_ADD_NUMBER_TO_OBJECT(client, "lastQuery", sqlite3_column_int64(stmt, 2));
JSON_COPY_STR_TO_OBJECT(client, "addresses", sqlite3_column_text(stmt, 3));
JSON_COPY_STR_TO_OBJECT(client, "names", sqlite3_column_text(stmt, 4));
JSON_ADD_ITEM_TO_ARRAY(clients, client);
Expand Down
4 changes: 3 additions & 1 deletion src/api/padd.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ int api_padd(struct ftl_conn *api)
if(config.misc.privacylevel.v.privacy_level < PRIVACY_HIDE_DOMAINS)
{
// Find most recently blocked query
for(int queryID = counters->queries - 1; queryID > 0 ; queryID--)
for(int queryID = counters->queries - 1; queryID >= 0 ; queryID--)
{
const queriesData *query = getQuery(queryID, true);
if(query == NULL)
Expand Down Expand Up @@ -98,6 +98,7 @@ int api_padd(struct ftl_conn *api)
const char *domain = cJSON_GetStringValue(top_block);
JSON_COPY_STR_TO_OBJECT(json, "top_blocked", domain);
}
cJSON_Delete(top_blocked);
cJSON *top_clients = get_top_clients(api, 1, false, true, false, true);
if(cJSON_GetArraySize(top_clients) == 0)
{
Expand All @@ -109,6 +110,7 @@ int api_padd(struct ftl_conn *api)
const char *client = cJSON_GetStringValue(top_client);
JSON_COPY_STR_TO_OBJECT(json, "top_client", client);
}
cJSON_Delete(top_clients);

// Add a null entry if the domain is hidden or there is no recent
// blocked domain (e.g. when blocking is disabled)
Expand Down
135 changes: 110 additions & 25 deletions src/api/stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ static int __attribute__((pure)) cmpdesc_te(const void *a, const void *b)
return 0;
}

// Min-heap sift-down for top-K selection: maintain the smallest element at the
// root so it can be replaced when a larger one is found
Comment thread
yubiuser marked this conversation as resolved.
static void heap_sift_down(struct top_entries *heap, const unsigned int size, unsigned int i)
{
for(;;)
{
unsigned int min = i;
const unsigned int l = 2*i + 1, r = 2*i + 2;
if(l < size && heap[l].count < heap[min].count)
min = l;
if(r < size && heap[r].count < heap[min].count)
min = r;
if(min == i)
break;
const struct top_entries tmp = heap[i];
heap[i] = heap[min];
heap[min] = tmp;
i = min;
}
}

static int get_query_types_obj(struct ftl_conn *api, cJSON *types)
{
for(unsigned int i = TYPE_A; i < TYPE_MAX; i++)
Expand Down Expand Up @@ -210,14 +231,21 @@ cJSON *get_top_domains(struct ftl_conn *api, const int count,
const unsigned int domains = counters->domains;
const unsigned int total_queries = counters->queries;
const unsigned int blocked_count = get_blocked_count();
struct top_entries *top_domains = calloc(domains, sizeof(struct top_entries));
if(top_domains == NULL)

// Heap-based top-K selection: allocate only for the top entries
// rather than for all domains, reducing memory from O(N) to O(K).
const unsigned int k = count > 0 ? (unsigned int)count : 0u;
Comment thread
DL6ER marked this conversation as resolved.
Outdated
const unsigned int heap_cap = (k <= domains / 4) ? k * 4 : domains;
struct top_entries *top_domains = heap_cap > 0 ? calloc(heap_cap, sizeof(struct top_entries)) : NULL;
if(heap_cap > 0 && top_domains == NULL)
{
log_err("Memory allocation failed in %s()", __FUNCTION__);
unlock_shm();
return NULL;
}

unsigned int added_domains = 0u;
unsigned int heap_size = 0;
bool heap_ready = false;
for(unsigned int domainID = 0; domainID < domains; domainID++)
{
// Get domain pointer
Expand All @@ -232,28 +260,52 @@ cJSON *get_top_domains(struct ftl_conn *api, const int count,
continue;

// Use either blocked or total count based on request string
top_domains[added_domains].count = blocked ? domain->blockedcount : domain->count - domain->blockedcount;
const int entry_count = blocked ? domain->blockedcount : domain->count - domain->blockedcount;

// Get domain name
top_domains[added_domains].namepos = domain->domainpos;
// Skip zero-count entries early
if(entry_count < 1)
continue;

// Increment counter
added_domains++;
if(heap_size < heap_cap)
{
// Heap not full yet, append directly
top_domains[heap_size].count = entry_count;
top_domains[heap_size].namepos = domain->domainpos;
heap_size++;
}
else
{
// Build min-heap once on first overflow
if(!heap_ready)
{
for(int j = (int)(heap_size / 2) - 1; j >= 0; j--)
heap_sift_down(top_domains, heap_size, (unsigned int)j);
heap_ready = true;
}
// Replace root (minimum) if this entry is larger
if(entry_count > top_domains[0].count)
{
top_domains[0].count = entry_count;
top_domains[0].namepos = domain->domainpos;
heap_sift_down(top_domains, heap_size, 0);
}
}
}

// Unlock shared memory
unlock_shm();

// Sort temporary array
qsort(top_domains, added_domains, sizeof(*top_domains), cmpdesc_te);
// Sort the small heap array descending
if(heap_size > 1)
qsort(top_domains, heap_size, sizeof(*top_domains), cmpdesc_te);

int n = 0;
cJSON *jtop_domains = cJSON_CreateArray();

// Lock shared memory
lock_shm();

for(unsigned int i = 0; i < added_domains; i++)
for(unsigned int i = 0; i < heap_size; i++)
{
// Skip e.g. recycled domains
if(top_domains[i].namepos == 0)
Expand Down Expand Up @@ -376,14 +428,21 @@ cJSON *get_top_clients(struct ftl_conn *api, const int count,
const unsigned int clients = counters->clients;
const int total_queries = counters->queries;
const int blocked_count = get_blocked_count();
struct top_entries *top_clients = calloc(clients, sizeof(struct top_entries));
if(top_clients == NULL)

// Heap-based top-K selection: allocate only for the top entries
// rather than for all clients, reducing memory from O(N) to O(K).
const unsigned int k = count > 0 ? (unsigned int)count : 0u;
Comment thread
DL6ER marked this conversation as resolved.
Outdated
const unsigned int heap_cap = (k <= clients / 4) ? k * 4 : clients;
struct top_entries *top_clients = heap_cap > 0 ? calloc(heap_cap, sizeof(struct top_entries)) : NULL;
if(heap_cap > 0 && top_clients == NULL)
{
log_err("Memory allocation failed in %s()", __FUNCTION__);
unlock_shm();
return 0;
}

unsigned int added_clients = 0;
unsigned int heap_size = 0;
bool heap_ready = false;
for(unsigned int clientID = 0; clientID < clients; clientID++)
{
// Get client pointer
Expand Down Expand Up @@ -413,22 +472,48 @@ cJSON *get_top_clients(struct ftl_conn *api, const int count,
}

// Use either blocked or total count based on request string
top_clients[added_clients].count = blocked ? client->blockedcount : client->count;
const int entry_count = blocked ? client->blockedcount : client->count;

// Get client name and IP
top_clients[added_clients].ippos = client->ippos;
top_clients[added_clients].namepos = client->namepos;
// Skip zero-count entries early
if(entry_count < 1)
continue;

added_clients++;
if(heap_size < heap_cap)
{
// Heap not full yet, append directly
top_clients[heap_size].count = entry_count;
top_clients[heap_size].ippos = client->ippos;
top_clients[heap_size].namepos = client->namepos;
heap_size++;
}
else
{
// Build min-heap once on first overflow
if(!heap_ready)
{
for(int j = (int)(heap_size / 2) - 1; j >= 0; j--)
heap_sift_down(top_clients, heap_size, (unsigned int)j);
heap_ready = true;
}
// Replace root (minimum) if this entry is larger
if(entry_count > top_clients[0].count)
{
top_clients[0].count = entry_count;
top_clients[0].ippos = client->ippos;
top_clients[0].namepos = client->namepos;
heap_sift_down(top_clients, heap_size, 0);
}
}
}

log_debug(DEBUG_API, "Found %u clients", added_clients);
log_debug(DEBUG_API, "Found %u clients (heap selected from %u)", heap_size, clients);

// Unlock shared memory
unlock_shm();

// Sort temporary array
qsort(top_clients, added_clients, sizeof(*top_clients), cmpdesc_te);
// Sort the small heap array descending
if(heap_size > 1)
qsort(top_clients, heap_size, sizeof(*top_clients), cmpdesc_te);

// Get clients which the user doesn't want to see
regex_t *regex_clients = NULL;
Expand All @@ -443,7 +528,7 @@ cJSON *get_top_clients(struct ftl_conn *api, const int count,
// Lock shared memory
lock_shm();

for(unsigned int i = 0; i < added_clients; i++)
for(unsigned int i = 0; i < heap_size; i++)
{
const char *client_ip = getstr(top_clients[i].ippos);
const char *client_name = getstr(top_clients[i].namepos);
Expand Down Expand Up @@ -596,7 +681,7 @@ cJSON *get_top_upstreams(struct ftl_conn *api, const bool upstreams_only)
unlock_shm();

// Sort temporary array in descending order
qsort(top_upstreams, added_upstreams, sizeof(*top_upstreams), cmpdesc);
qsort(top_upstreams, added_upstreams, sizeof(*top_upstreams), cmpdesc_te);

// Loop over available forward destinations
cJSON *jtop_upstreams = JSON_NEW_ARRAY();
Expand Down Expand Up @@ -770,7 +855,7 @@ int api_stats_recentblocked(struct ftl_conn *api)
// Find most recently blocked query
unsigned int found = 0;
cJSON *blocked = JSON_NEW_ARRAY();
for(int queryID = counters->queries - 1; queryID > 0 ; queryID--)
for(int queryID = counters->queries - 1; queryID >= 0 ; queryID--)
{
const queriesData *query = getQuery(queryID, true);
if(query == NULL)
Expand Down
Loading
Loading