From 82dcecc689f243d78e4aaad6416b66c1c6fd0789 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:33:47 +0000 Subject: [PATCH 1/4] Initial plan From 81ead947fa857b5d104485d4c2e9e0369e6a994c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:39:40 +0000 Subject: [PATCH 2/4] Optimize nested loops and string concatenation for better performance Co-authored-by: kasnder <5175206+kasnder@users.noreply.github.com> --- .../eu/faircode/netguard/AdapterRule.java | 16 +- .../eu/faircode/netguard/DatabaseHelper.java | 161 ++++++++++-------- .../missioncontrol/data/BlocklistManager.java | 16 +- 3 files changed, 107 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 2ffc88e8..1065732f 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -1039,10 +1039,19 @@ private void updateRule(Context context, Rule rule, boolean root, List lis rule.updateChanged(context); Log.i(TAG, "Updated " + rule); + // Optimize: Use HashMap to avoid O(n*m) nested loop complexity List listModified = new ArrayList<>(); - for (String pkg : rule.related) { - for (Rule related : listAll) - if (related.packageName.equals(pkg)) { + if (!rule.related.isEmpty()) { + // Build a map for O(1) lookup + java.util.HashMap packageMap = new java.util.HashMap<>(); + for (Rule r : listAll) { + packageMap.put(r.packageName, r); + } + + // Find and update related rules in O(n) time + for (String pkg : rule.related) { + Rule related = packageMap.get(pkg); + if (related != null) { related.wifi_blocked = rule.wifi_blocked; related.other_blocked = rule.other_blocked; related.apply = rule.apply; @@ -1053,6 +1062,7 @@ private void updateRule(Context context, Rule rule, boolean root, List lis related.notify = rule.notify; listModified.add(related); } + } } List listSearch = (root ? new ArrayList<>(listAll) : listAll); diff --git a/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java b/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java index 58567186..5fb8febe 100644 --- a/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java +++ b/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java @@ -501,23 +501,24 @@ public Cursor getLog(boolean udp, boolean tcp, boolean other, boolean allowed, b SQLiteDatabase db = this.getReadableDatabase(); // There is an index on time // There is no index on protocol/allowed for write performance - String query = "SELECT ID AS _id, *"; - query += " FROM log"; - query += " WHERE (0 = 1"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM log"); + query.append(" WHERE (0 = 1"); if (udp) - query += " OR protocol = 17"; + query.append(" OR protocol = 17"); if (tcp) - query += " OR protocol = 6"; + query.append(" OR protocol = 6"); if (other) - query += " OR (protocol <> 6 AND protocol <> 17)"; - query += ") AND (0 = 1"; + query.append(" OR (protocol <> 6 AND protocol <> 17)"); + query.append(") AND (0 = 1"); if (allowed) - query += " OR allowed = 1"; + query.append(" OR allowed = 1"); if (blocked) - query += " OR allowed = 0"; - query += ")"; - query += " ORDER BY time DESC"; - return db.rawQuery(query, new String[] {}); + query.append(" OR allowed = 0"); + query.append(")"); + query.append(" ORDER BY time DESC"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } @@ -528,11 +529,12 @@ public Cursor searchLog(String find) { try { SQLiteDatabase db = this.getReadableDatabase(); // There is an index on daddr, dname, dport and uid - String query = "SELECT ID AS _id, *"; - query += " FROM log"; - query += " WHERE daddr LIKE ? OR dname LIKE ? OR dport = ? OR uid = ?"; - query += " ORDER BY time DESC"; - return db.rawQuery(query, new String[] { "%" + find + "%", "%" + find + "%", find, find }); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM log"); + query.append(" WHERE daddr LIKE ? OR dname LIKE ? OR dport = ? OR uid = ?"); + query.append(" ORDER BY time DESC"); + return db.rawQuery(query.toString(), new String[] { "%" + find + "%", "%" + find + "%", find, find }); } finally { lock.readLock().unlock(); } @@ -740,13 +742,14 @@ public Cursor getAccess(int uid) { SQLiteDatabase db = this.getReadableDatabase(); // There is a segmented index on uid // There is no index on time for write performance - String query = "SELECT a.ID AS _id, a.*"; - query += ", (SELECT COUNT(DISTINCT d.qname) FROM dns d WHERE d.resource IN (SELECT d1.resource FROM dns d1 WHERE d1.qname = a.daddr)) count"; - query += " FROM access a"; - query += " WHERE a.uid = ?"; - query += " ORDER BY a.time DESC"; - query += " LIMIT 250"; - return db.rawQuery(query, new String[] { Integer.toString(uid) }); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT a.ID AS _id, a.*"); + query.append(", (SELECT COUNT(DISTINCT d.qname) FROM dns d WHERE d.resource IN (SELECT d1.resource FROM dns d1 WHERE d1.qname = a.daddr)) count"); + query.append(" FROM access a"); + query.append(" WHERE a.uid = ?"); + query.append(" ORDER BY a.time DESC"); + query.append(" LIMIT 250"); + return db.rawQuery(query.toString(), new String[] { Integer.toString(uid) }); } finally { lock.readLock().unlock(); } @@ -770,16 +773,17 @@ public Cursor getAccessUnset(int uid, int limit, long since) { SQLiteDatabase db = this.getReadableDatabase(); // There is a segmented index on uid, block and daddr // There is no index on allowed and time for write performance - String query = "SELECT MAX(time) AS time, daddr, allowed"; - query += " FROM access"; - query += " WHERE uid = ?"; - query += " AND block < 0"; - query += " AND time >= ?"; - query += " GROUP BY daddr, allowed"; - query += " ORDER BY time DESC"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT MAX(time) AS time, daddr, allowed"); + query.append(" FROM access"); + query.append(" WHERE uid = ?"); + query.append(" AND block < 0"); + query.append(" AND time >= ?"); + query.append(" GROUP BY daddr, allowed"); + query.append(" ORDER BY time DESC"); if (limit > 0) - query += " LIMIT " + limit; - return db.rawQuery(query, new String[] { Integer.toString(uid), Long.toString(since) }); + query.append(" LIMIT ").append(limit); + return db.rawQuery(query.toString(), new String[] { Integer.toString(uid), Long.toString(since) }); } finally { lock.readLock().unlock(); } @@ -923,14 +927,15 @@ public String getQName(int uid, String ip) { readableDb = this.getReadableDatabase(); SQLiteDatabase db = readableDb; // There is a segmented index on resource - String query = "SELECT d.qname"; - query += " FROM dns AS d"; - query += " WHERE d.resource = '" + ip.replace("'", "''") + "'"; - query += " ORDER BY d.qname"; - query += " LIMIT 1"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT d.qname"); + query.append(" FROM dns AS d"); + query.append(" WHERE d.resource = '").append(ip.replace("'", "''")).append("'"); + query.append(" ORDER BY d.qname"); + query.append(" LIMIT 1"); // There is no way to known for sure which domain name an app used, so just pick // the first one - return db.compileStatement(query).simpleQueryForString(); + return db.compileStatement(query.toString()).simpleQueryForString(); } catch (SQLiteDoneException ignored) { // Not found return null; @@ -947,14 +952,15 @@ public Cursor getQAName(int uid, String ip, boolean alive) { readableDb = this.getReadableDatabase(); SQLiteDatabase db = readableDb; // There is a segmented index on resource - String query = "SELECT d.qname, d.aname, d.time, d.ttl"; - query += " FROM dns AS d"; - query += " WHERE d.resource = '" + ip.replace("'", "''") + "'"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT d.qname, d.aname, d.time, d.ttl"); + query.append(" FROM dns AS d"); + query.append(" WHERE d.resource = '").append(ip.replace("'", "''")).append("'"); if (alive) - query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; - query += " GROUP BY d.qname"; // remove duplicates - query += " ORDER BY d.qname"; - return db.rawQuery(query, new String[] {}); + query.append(" AND (d.time IS NULL OR d.time + d.ttl >= ").append(now).append(")"); + query.append(" GROUP BY d.qname"); // remove duplicates + query.append(" ORDER BY d.qname"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } @@ -964,13 +970,14 @@ public Cursor getAlternateQNames(String qname) { lock.readLock().lock(); try { SQLiteDatabase db = this.getReadableDatabase(); - String query = "SELECT DISTINCT d2.qname"; - query += " FROM dns d1"; - query += " JOIN dns d2"; - query += " ON d2.resource = d1.resource AND d2.id <> d1.id"; - query += " WHERE d1.qname = ?"; - query += " ORDER BY d2.qname"; - return db.rawQuery(query, new String[] { qname }); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT DISTINCT d2.qname"); + query.append(" FROM dns d1"); + query.append(" JOIN dns d2"); + query.append(" ON d2.resource = d1.resource AND d2.id <> d1.id"); + query.append(" WHERE d1.qname = ?"); + query.append(" ORDER BY d2.qname"); + return db.rawQuery(query.toString(), new String[] { qname }); } finally { lock.readLock().unlock(); } @@ -981,13 +988,14 @@ public Cursor getAName(String qname, boolean alive) { lock.readLock().lock(); try { SQLiteDatabase db = this.getReadableDatabase(); - String query = "SELECT d.qname, d.aname, d.time, d.ttl"; - query += " FROM dns d"; - query += " WHERE d.qname = ?"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT d.qname, d.aname, d.time, d.ttl"); + query.append(" FROM dns d"); + query.append(" WHERE d.qname = ?"); if (alive) - query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; - query += " LIMIT 1"; - return db.rawQuery(query, new String[] { qname }); + query.append(" AND (d.time IS NULL OR d.time + d.ttl >= ").append(now).append(")"); + query.append(" LIMIT 1"); + return db.rawQuery(query.toString(), new String[] { qname }); } finally { lock.readLock().unlock(); } @@ -999,10 +1007,11 @@ public Cursor getDns() { SQLiteDatabase db = this.getReadableDatabase(); // There is an index on resource // There is a segmented index on qname - String query = "SELECT ID AS _id, *"; - query += " FROM dns"; - query += " ORDER BY resource, qname"; - return db.rawQuery(query, new String[] {}); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM dns"); + query.append(" ORDER BY resource, qname"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } @@ -1016,16 +1025,17 @@ public Cursor getAccessDns(String dname) { // There is a segmented index on dns.qname // There is an index on access.daddr and access.block - String query = "SELECT a.uid, a.version, a.protocol, a.daddr, d.resource, a.dport, a.block, d.time, d.ttl"; - query += " FROM access AS a"; - query += " LEFT JOIN dns AS d"; - query += " ON d.qname = a.daddr"; - query += " WHERE a.block >= 0"; - query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT a.uid, a.version, a.protocol, a.daddr, d.resource, a.dport, a.block, d.time, d.ttl"); + query.append(" FROM access AS a"); + query.append(" LEFT JOIN dns AS d"); + query.append(" ON d.qname = a.daddr"); + query.append(" WHERE a.block >= 0"); + query.append(" AND (d.time IS NULL OR d.time + d.ttl >= ").append(now).append(")"); if (dname != null) - query += " AND a.daddr = ?"; + query.append(" AND a.daddr = ?"); - return db.rawQuery(query, dname == null ? new String[] {} : new String[] { dname }); + return db.rawQuery(query.toString(), dname == null ? new String[] {} : new String[] { dname }); } finally { lock.readLock().unlock(); } @@ -1103,10 +1113,11 @@ public Cursor getForwarding() { lock.readLock().lock(); try { SQLiteDatabase db = this.getReadableDatabase(); - String query = "SELECT ID AS _id, *"; - query += " FROM forward"; - query += " ORDER BY dport"; - return db.rawQuery(query, new String[] {}); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM forward"); + query.append(" ORDER BY dport"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } diff --git a/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java b/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java index f9d2f56d..61ea6b4d 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java +++ b/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java @@ -41,16 +41,16 @@ private void cleanup() { if (files == null) return; + // Optimize: Use HashSet to avoid O(n*m) nested loop complexity List list = getBlocklists(); + java.util.HashSet validFiles = new java.util.HashSet<>(); + for (Blocklist item : list) { + validFiles.add("blocklist_" + item.uuid + ".txt"); + } + + // Check each file in O(1) time using HashSet for (File file : files) { - boolean found = false; - for (Blocklist item : list) { - if (file.getName().equals("blocklist_" + item.uuid + ".txt")) { - found = true; - break; - } - } - if (!found) { + if (!validFiles.contains(file.getName())) { Log.i(TAG, "Deleting orphaned file " + file.getName()); file.delete(); } From fc9aea5ffd0df31c4c605fc64c70b186b7d5d24f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:41:30 +0000 Subject: [PATCH 3/4] Cache sorted hosts in Tracker class to avoid repeated sorting in UI Co-authored-by: kasnder <5175206+kasnder@users.noreply.github.com> --- .../kollnig/missioncontrol/data/Tracker.java | 20 +++++++++++++++++++ .../details/TrackersListAdapter.java | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java b/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java index 3cfaab02..9199f510 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java +++ b/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java @@ -19,7 +19,10 @@ import androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -27,6 +30,7 @@ */ public class Tracker { private final Set hosts = new HashSet<>(); + private List sortedHostsCache = null; // Cache sorted hosts for performance public String name; public String category; public Long lastSeen; @@ -97,6 +101,8 @@ public String getCategory() { */ void addHost(String host) { this.hosts.add(host); + // Invalidate cache when host is added + sortedHostsCache = null; } /** @@ -107,4 +113,18 @@ void addHost(String host) { public Set getHosts() { return hosts; } + + /** + * Get sorted list of hosts. This method caches the sorted result for performance. + * Calling this repeatedly is more efficient than sorting in UI code. + * + * @return Sorted list of hosts + */ + public List getSortedHosts() { + if (sortedHostsCache == null) { + sortedHostsCache = new ArrayList<>(hosts); + Collections.sort(sortedHostsCache); + } + return sortedHostsCache; + } } diff --git a/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java b/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java index 532c78e4..6006597a 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java +++ b/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java @@ -276,8 +276,8 @@ private void updateText(TextView tv, Tracker t) { if (t.lastSeen != 0) title += " (" + Util.relativeTime(t.lastSeen) + ")"; - List sortedHosts = new ArrayList<>(t.getHosts()); - Collections.sort(sortedHosts); + // Optimize: Use cached sorted hosts instead of creating new list and sorting every time + List sortedHosts = t.getSortedHosts(); String hosts = TextUtils.join("\n• ", sortedHosts); boolean categoryBlocked = b.blocked(mAppUid, trackerCategoryName); From 3ce2780695912f558b9eb26859f197706e7e8882 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:27:09 +0000 Subject: [PATCH 4/4] Add runtime cache invalidation check for hosts modifications Track hosts Set size to detect external modifications and automatically invalidate the sorted hosts cache when size changes. This ensures the cache stays consistent even if hosts are modified through getHosts(). Co-authored-by: kasnder <5175206+kasnder@users.noreply.github.com> --- .../java/net/kollnig/missioncontrol/data/Tracker.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java b/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java index 9199f510..c03efdb7 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java +++ b/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java @@ -31,6 +31,7 @@ public class Tracker { private final Set hosts = new HashSet<>(); private List sortedHostsCache = null; // Cache sorted hosts for performance + private int cachedHostsSize = -1; // Track size to detect external modifications public String name; public String category; public Long lastSeen; @@ -103,6 +104,7 @@ void addHost(String host) { this.hosts.add(host); // Invalidate cache when host is added sortedHostsCache = null; + cachedHostsSize = -1; } /** @@ -117,13 +119,20 @@ public Set getHosts() { /** * Get sorted list of hosts. This method caches the sorted result for performance. * Calling this repeatedly is more efficient than sorting in UI code. + * The cache is automatically invalidated if hosts are modified. * * @return Sorted list of hosts */ public List getSortedHosts() { + // Invalidate cache if hosts size changed (detects external modifications) + if (sortedHostsCache != null && hosts.size() != cachedHostsSize) { + sortedHostsCache = null; + } + if (sortedHostsCache == null) { sortedHostsCache = new ArrayList<>(hosts); Collections.sort(sortedHostsCache); + cachedHostsSize = hosts.size(); } return sortedHostsCache; }