Skip to content

Commit 5b3dd83

Browse files
committed
Fix CPU / thread locks + improve performance of BrowserBookmark plugin
## 1. High CPU and UI Freezing * **Initial Load Spike:** The original plugin caused a massive CPU spike on first load due to inefficient database queries for favicons. **Fix:** We now create temporary indexes on the database copies, making the initial favicon scan virtually instantaneous. * **Query-Time CPU Usage:** Typing in the search bar caused high CPU because the plugin checked for the existence of every icon file on every keystroke. **Fix:** We now pre-validate all icon paths once during the initial load, making search queries extremely fast, in-memory operations. * **Background CPU Churn:** Actively browsing the web would trigger constant, high-CPU reloads of the plugin's data. **Fix:** We implemented a debouncing mechanism for the file watcher, ensuring that even a storm of file changes only results in a single, efficient data reload. * **UI Freezing on Scroll (The Final Bug):** The most severe issue was the UI locking up with 100% CPU on multiple threads when scrolling through results. This was caused by the UI's renderer (SharpVectors) attempting to render corrupt SVG favicons created by a race condition. **Fix:** We now convert **all** favicons to PNG format during the loading process. This guarantees that the UI never receives a corrupt or incompatible image, permanently solving the freezing issue. ## 2. Improved Reliability and Functionality * **Fixed Firefox Profile Discovery:** The plugin now reliably and automatically discovers the default Firefox profile, even in non-standard locations or with different naming schemes, by correctly parsing the `profiles.ini` file. * **Fixed Real-Time Settings Changes:** Toggling the "Load favicons" checkbox now triggers an immediate data reload, so the change takes effect instantly without requiring an application restart. ## 3. Enhanced Code Quality and User Experience * **Asynchronous Startup:** The entire data loading process is now fully asynchronous. The plugin displays an "initializing..." message while loading in the background, ensuring the Flow Launcher UI remains responsive at all times. * **Code Stability:** eliminated unsafe `async void` methods, fixed various compilation errors, and centralized duplicated code into a shared helper class, making the plugin more stable and maintainable.
1 parent d0c733b commit 5b3dd83

File tree

8 files changed

+373
-404
lines changed

8 files changed

+373
-404
lines changed

Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs

Lines changed: 37 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
2-
using System.Collections.Concurrent;
32
using System.Collections.Generic;
43
using System.IO;
54
using System.Text.Json;
6-
using System.Threading.Tasks;
75
using Flow.Launcher.Plugin.BrowserBookmark.Helper;
86
using Flow.Launcher.Plugin.BrowserBookmark.Models;
97
using Microsoft.Data.Sqlite;
@@ -78,10 +76,18 @@ protected static List<Bookmark> LoadBookmarksFromFile(string path, string source
7876
if (!File.Exists(path))
7977
return bookmarks;
8078

81-
using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
82-
if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
83-
return bookmarks;
84-
EnumerateRoot(rootElement, bookmarks, source);
79+
try
80+
{
81+
using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
82+
if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
83+
return bookmarks;
84+
EnumerateRoot(rootElement, bookmarks, source);
85+
}
86+
catch (JsonException e)
87+
{
88+
Main._context.API.LogException(ClassName, $"Failed to parse bookmarks file: {path}", e);
89+
}
90+
8591
return bookmarks;
8692
}
8793

@@ -115,91 +121,41 @@ private static void EnumerateFolderBookmark(JsonElement folderElement, ICollecti
115121
case "workspace": // Edge Workspace
116122
EnumerateFolderBookmark(subElement, bookmarks, source);
117123
break;
118-
default:
119-
bookmarks.Add(new Bookmark(
120-
subElement.GetProperty("name").GetString(),
121-
subElement.GetProperty("url").GetString(),
122-
source));
124+
case "url":
125+
if (subElement.TryGetProperty("name", out var name) &&
126+
subElement.TryGetProperty("url", out var url))
127+
{
128+
bookmarks.Add(new Bookmark(name.GetString(), url.GetString(), source));
129+
}
123130
break;
124131
}
125132
}
126133
else
127134
{
128-
Main._context.API.LogError(ClassName, $"type property not found for {subElement.GetString()}");
135+
Main._context.API.LogError(ClassName, $"type property not found for {subElement.ToString()}");
129136
}
130137
}
131138
}
132139

133140
private void LoadFaviconsFromDb(string dbPath, List<Bookmark> bookmarks)
134141
{
135-
FaviconHelper.LoadFaviconsFromDb(_faviconCacheDir, dbPath, (tempDbPath) =>
136-
{
137-
// Since some bookmarks may have same favicon id, we need to record them to avoid duplicates
138-
var savedPaths = new ConcurrentDictionary<string, bool>();
139-
140-
// Get favicons based on bookmarks concurrently
141-
Parallel.ForEach(bookmarks, bookmark =>
142-
{
143-
// Use read-only connection to avoid locking issues
144-
// Do not use pooling so that we do not need to clear pool: https://github.com/dotnet/efcore/issues/26580
145-
var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly;Pooling=false");
146-
connection.Open();
147-
148-
try
149-
{
150-
var url = bookmark.Url;
151-
if (string.IsNullOrEmpty(url)) return;
152-
153-
// Extract domain from URL
154-
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
155-
return;
156-
157-
var domain = uri.Host;
158-
159-
using var cmd = connection.CreateCommand();
160-
cmd.CommandText = @"
161-
SELECT f.id, b.image_data
162-
FROM favicons f
163-
JOIN favicon_bitmaps b ON f.id = b.icon_id
164-
JOIN icon_mapping m ON f.id = m.icon_id
165-
WHERE m.page_url LIKE @url
166-
ORDER BY b.width DESC
167-
LIMIT 1";
168-
169-
cmd.Parameters.AddWithValue("@url", $"%{domain}%");
170-
171-
using var reader = cmd.ExecuteReader();
172-
if (!reader.Read() || reader.IsDBNull(1))
173-
return;
174-
175-
var iconId = reader.GetInt64(0).ToString();
176-
var imageData = (byte[])reader["image_data"];
177-
178-
if (imageData is not { Length: > 0 })
179-
return;
180-
181-
var faviconPath = Path.Combine(_faviconCacheDir, $"chromium_{domain}_{iconId}.png");
182-
183-
// Filter out duplicate favicons
184-
if (savedPaths.TryAdd(faviconPath, true))
185-
{
186-
FaviconHelper.SaveBitmapData(imageData, faviconPath);
187-
}
188-
189-
bookmark.FaviconPath = faviconPath;
190-
}
191-
catch (Exception ex)
192-
{
193-
Main._context.API.LogException(ClassName, $"Failed to extract bookmark favicon: {bookmark.Url}", ex);
194-
}
195-
finally
196-
{
197-
// Cache connection and clear pool after all operations to avoid issue:
198-
// ObjectDisposedException: Safe handle has been closed.
199-
connection.Close();
200-
connection.Dispose();
201-
}
202-
});
203-
});
142+
const string sql = @"
143+
SELECT f.id, b.image_data
144+
FROM favicons f
145+
JOIN favicon_bitmaps b ON f.id = b.icon_id
146+
JOIN icon_mapping m ON f.id = m.icon_id
147+
WHERE m.page_url GLOB @pattern
148+
ORDER BY b.width DESC
149+
LIMIT 1";
150+
151+
FaviconHelper.ProcessFavicons(
152+
dbPath,
153+
_faviconCacheDir,
154+
bookmarks,
155+
sql,
156+
"http*",
157+
reader => (reader.GetInt64(0).ToString(), (byte[])reader["image_data"]),
158+
(uri, id, data) => Path.Combine(_faviconCacheDir, $"chromium_{uri.Host}_{id}.png")
159+
);
204160
}
205161
}

0 commit comments

Comments
 (0)