Skip to content

Commit 5a23761

Browse files
committed
fix logger memory issues
1 parent 12feb42 commit 5a23761

3 files changed

Lines changed: 64 additions & 100 deletions

File tree

include/ui/mainwindow.h

Lines changed: 9 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,10 @@ namespace Ui {
6060
QT_END_NAMESPACE
6161

6262
enum class RefreshAnchor {
63-
// Re-select the same profiles by id; select nothing if they are gone.
6463
KeepPlace,
65-
// As above, but if all of them were deleted select whatever took their row.
6664
Removal,
6765
};
6866

69-
// What the app launches in place of itself once on_menu_exit_triggered() has torn
70-
// it down. Doubles as get_elevated_permissions()'s reason: on Windows that exits
71-
// and relaunches elevated with the matching flag.
7267
enum class ExitReason {
7368
None,
7469
RunUpdater,
@@ -85,9 +80,6 @@ class MainWindow : public QMainWindow {
8580

8681
~MainWindow() override;
8782

88-
// Runtime Stats panel helpers, read on the UI thread. GetCorePid returns 0
89-
// when the core process isn't running; GetRunningConfigName is empty when no
90-
// profile is active.
9183
qint64 GetCorePid();
9284
QString GetRunningConfigName();
9385

@@ -130,11 +122,8 @@ class MainWindow : public QMainWindow {
130122

131123
void UpdateDataView(bool force = false);
132124

133-
// Pushes the auto-selector snapshot into the data view, toggles the Tools
134-
// entry, and refreshes the dialog if it is open.
135125
void refresh_auto_selector_view();
136126

137-
// Non-owning: cleared by the dialog's finished() handler.
138127
class DialogAutoSelector *m_autoSelectorDialog = nullptr;
139128

140129
void setDownloadReport(const DownloadProgressReport& report, bool show);
@@ -213,27 +202,21 @@ private slots:
213202

214203
private:
215204
Ui::MainWindow *ui;
216-
// Monotonic, and invalid while the window is active or was never activated; see trayClickEvent().
217205
QElapsedTimer sinceWindowDeactivated;
218206
ProfilesTableModel *profilesTableModel = nullptr;
219-
// What the view is attached to: rows from the view or its selection model are
220-
// proxy rows, not profilesTableModel rows.
207+
221208
ProfilesFilterProxyModel *profilesFilterModel = nullptr;
222209
QSystemTrayIcon *tray;
223210
QMenu *trayMenu = nullptr; // tray context menu
224-
// Tray "Select Server"/"Select Routing" open this small Qt-drawn popup instead of a
225-
// submenu, because a tray submenu isn't painted by Qt on Linux (SNI/DBusMenu) or macOS
226-
// (native NSMenu) and so can't reliably expand a dynamic list. Recreated on each open.
227211
QPointer<TrayProfileSelector> traySelector;
228212
void openTraySelector(bool routing);
229213
QShortcut *shortcut_esc = new QShortcut(QKeySequence::Cancel, this);
230214
//
231-
// Shared by the test sweeps and the batch profile scans (remove-invalid).
232215
QThreadPool *parallelCoreCallPool = new QThreadPool(this);
233216
std::unique_ptr<TestRunner> testRunner;
234217
//
235218
Configs_sys::CoreProcess *core_process = nullptr;
236-
QMutex coreProcessMutex; // serializes core_process init (DS_cores) vs IPC newConnection (UI)
219+
QMutex coreProcessMutex;
237220
QLocalServer *core_server = nullptr;
238221
bool rpc_started = false;
239222
qint64 vpn_pid = 0;
@@ -244,14 +227,8 @@ private slots:
244227
int icon_status = -1;
245228
std::shared_ptr<Configs::Profile> running;
246229
int last_running_profile_id = -1;
247-
// True from the moment a profile start is kicked off until it succeeds or
248-
// fails; drives the start/stop button's transient "Connecting" state.
249230
bool m_profileConnecting = false;
250-
// True while a profile stop is in progress; drives the "Disconnecting" state.
251231
bool m_profileDisconnecting = false;
252-
// Single-flight guard for the Xray geo-asset (geoip.dat/geosite.dat) download
253-
// prompt: a batch test can surface the missing-asset error for many profiles at
254-
// once, and we only want one prompt/download. Touched on the UI thread only.
255232
bool m_xrayGeoAssetBusy = false;
256233
QString traffic_update_cache;
257234
qint64 last_test_time = 0;
@@ -272,7 +249,6 @@ private slots:
272249
SpeedWidget *speedChartWidget;
273250
//
274251
// for data view
275-
// Repaint throttle, in ms since epoch. Atomic: worker threads drive it too.
276252
std::atomic<qint64> lastUpdatedMs = QDateTime::currentMSecsSinceEpoch();
277253
DataViewHtmlGenerator dataViewHtmlGenerator_;
278254

@@ -287,7 +263,6 @@ private slots:
287263

288264
QTimer *m_filterRefreshDebounce = nullptr;
289265

290-
// Only meaningful between a saveProfileFocusState() and its restore.
291266
bool m_profilesTableHadFocus = false;
292267
int m_profilesScrollValue = 0;
293268

@@ -301,10 +276,10 @@ private slots:
301276
QWaitCondition logWaiter;
302277
Qv2ray::ui::SyntaxHighlighter *logHighlighter = nullptr;
303278

304-
// Immutable snapshot of the log filter fields. The log thread copies these
305-
// under logMutex (Qt containers are copy-on-write, so it's O(1)) and then
306-
// filters without holding the lock, so producers calling append_log() are
307-
// never blocked on the regex/keyword work.
279+
QMutex logPendingMutex;
280+
QString logPendingText;
281+
bool logFlushScheduled = false;
282+
308283
struct LogFilter {
309284
bool enableInclude = false;
310285
bool enableExclude = false;
@@ -318,12 +293,13 @@ private slots:
318293

319294
void log_process_loop();
320295

296+
// UI thread only.
297+
void flush_log_batch();
298+
321299
bool should_print_log(const QString &log, const LogFilter &filter);
322300

323301
void updateLogFilterFields();
324302

325-
// (Re)installs the log syntax highlighter, deleting any previous one so
326-
// highlighters don't stack up (and keep re-highlighting) on theme changes.
327303
void setLogHighlighter(bool darkMode);
328304

329305
void applyProfileFilters();
@@ -353,12 +329,8 @@ private slots:
353329

354330
void handle_import_route(const QString &url);
355331

356-
// throne://remoteRoute?data=<...> : add one or more remote routing profiles. The data is
357-
// (base64 of) a JSON array of {url, auto_update[, name]} objects.
358332
void handle_add_remote_routes(const QString &url);
359333

360-
// Routes user-supplied text: throne:// links go to the deeplink handler, the
361-
// rest to the subscription/profile importer.
362334
void import_or_handle_deeplink(const QString &text);
363335

364336
void refresh_proxy_list_column_size();
@@ -369,9 +341,6 @@ private slots:
369341

370342
void parseQrImage(const QPixmap *image);
371343

372-
// Imports local files picked from the file dialog or dropped on the window.
373-
// What each file is gets decided from its bytes, never from its name: config
374-
// files arrive as .json, .conf, .txt or with no extension at all.
375344
void importFromFiles(const QStringList &paths);
376345

377346
void trayClickEvent();
@@ -388,10 +357,6 @@ private slots:
388357

389358
void resizeEvent(QResizeEvent *event) override;
390359

391-
// Tell the connection lister whether its tab is actually on screen (stats tab
392-
// selected, window neither minimized nor hidden to tray) so it can drop to a
393-
// relaxed poll cadence when nobody is looking. Recomputed on tab/visibility
394-
// changes.
395360
void syncConnectionViewState();
396361

397362
void dragEnterEvent(QDragEnterEvent *event);
@@ -400,16 +365,10 @@ private slots:
400365

401366
void applyLogBrowserFont();
402367

403-
// Re-derives the top bar's sizing from the current font and translation, and
404-
// raises the window's minimum to whatever the layout actually needs. Called
405-
// at startup and on every font change.
406368
void applyTopBarMetrics();
407369

408-
// The window minimum the .ui was designed with; applyTopBarMetrics() only ever
409-
// grows past this, so a smaller font returns to the designed floor.
410370
QSize designMinimumSize;
411371

412-
// Debounced refresh_proxy_list trigger for font/theme/resize events.
413372
QTimer *m_proxyListRefreshDebounce = nullptr;
414373
void scheduleProxyListRefresh();
415374

@@ -420,15 +379,7 @@ private slots:
420379
void HotkeyEvent(const QString &key);
421380

422381
void RegisterHiddenMenuShortcuts(bool unregister = false);
423-
// Register a QShortcut for every action in `menu` (recursing into submenus),
424-
// appending them to hiddenMenuShortcuts. Needed because the menubar is hidden,
425-
// so actions reachable only through popup menus get no shortcut on their own.
426-
// `claimed` holds the key sequences already handled (either by Qt automatically
427-
// or by an earlier call); shortcuts already in it are skipped to avoid the
428-
// ambiguous-shortcut conflict that breaks actions shared with other menus.
429382
void registerMenuShortcuts(QMenu *menu, QSet<QKeySequence> &claimed);
430-
// Collect the shortcut key sequences of every action in `menu` (recursing into
431-
// submenus) into `out`, without registering anything.
432383
void collectMenuShortcuts(QMenu *menu, QSet<QKeySequence> &out);
433384

434385
void setActionsData();
@@ -443,29 +394,14 @@ private slots:
443394

444395
bool verify_core_pid(QLocalSocket *socket);
445396

446-
// Measures the members of an auto selector that have no test result yet
447-
// (plus `stale`, whose stored result is known to be out of date) and
448-
// rewrites its ranked pool. Blocks — call from a worker thread.
449397
void rank_auto_selector(const std::shared_ptr<Configs::Profile>& ent, const QList<int>& stale = {});
450398

451-
// Every running member of the auto selector died: re-rank and restart on
452-
// the next batch of good ones.
453399
void on_auto_selector_exhausted(int profileID);
454400

455-
// A subscription refresh rewrote the servers of `gid`. Drops ids that no
456-
// longer exist from every selector tracking that group, and rebuilds the
457-
// running one only if the refresh touched a member it actually built.
458-
// `disturbed` holds the profiles the refresh deleted or replaced in place.
459401
void on_subscription_group_changed(int gid, const QList<int>& disturbed);
460402

461-
// Guards the re-entrant profile_start used to rank before building.
462403
bool auto_selector_ranked = false;
463404

464-
// If `error` reports missing Xray geo assets (geoip.dat / geosite.dat), prompt
465-
// once (guarded by m_xrayGeoAssetBusy) and download the missing .dat files in
466-
// the background. Shared by profile start and the test paths. `contextName` is
467-
// the profile/config name shown in the prompt. Returns true when the error was
468-
// a geo-asset error (and thus handled), false otherwise.
469405
bool handleXrayGeoAssetError(const QString& error, const QString& contextName);
470406

471407
void url_test_current();
@@ -478,8 +414,6 @@ private slots:
478414

479415
void setupConnectionSortMenu();
480416

481-
// The window's own component, not an outside caller: it drives the data
482-
// view, the profile table and the geo-asset prompt while a sweep runs.
483417
friend class TestRunner;
484418

485419
protected:

src/global/Utils.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -355,18 +355,17 @@ void HideWindow(QWidget *w) {
355355
}
356356

357357
void runOnUiThread(const std::function<void()> &callback, bool wait) {
358-
// any thread. Targets qApp's thread rather than mainwindow's: they are the
359-
// same thread, but qApp exists for the whole of main() while mainwindow stays
360-
// null until UI_InitMainWindow(). Background work started before that (e.g.
361-
// the traffic-stats rollup) can report errors through here, and dereferencing
362-
// the null mainwindow crashed the worker thread.
363358
auto *app = QCoreApplication::instance();
364359
if (app == nullptr) return;
365360
auto thread = app->thread();
366361
if (thread == QThread::currentThread()) {
367362
callback();
368363
return;
369364
}
365+
if (!wait) {
366+
QMetaObject::invokeMethod(app, callback, Qt::QueuedConnection);
367+
return;
368+
}
370369
auto *timer = new QTimer();
371370
timer->moveToThread(thread);
372371
timer->setSingleShot(true);

src/ui/mainWindow/mainwindow_log.cpp

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#include "3rdparty/qv2ray/v2/ui/LogHighlighter.hpp"
1414

1515
namespace {
16+
constexpr qsizetype MAX_PENDING_LOG_CHARS = 2 * 1024 * 1024;
17+
1618
// Bypasses QTextEdit::append()'s per-call layout/scroll work, which dominates
1719
// when the core spams lines; one edit block per batch instead.
1820
inline void FastAppendTextDocument(const QString &message, QTextDocument *doc) {
@@ -81,26 +83,50 @@ void MainWindow::log_process_loop() {
8183
}
8284
}
8385

84-
if (!batchToPrint.isEmpty()) {
85-
QString trimmedBatch = batchToPrint.trimmed();
86-
runOnUiThread([trimmedBatch = std::move(trimmedBatch), this] {
87-
auto bar = ui->masterLogBrowser->verticalScrollBar();
88-
if (Configs::dataManager->settingsRepo->log_auto_scroll) {
89-
FastAppendTextDocument(trimmedBatch, qvLogDocument);
90-
bar->setValue(bar->maximum());
91-
} else {
92-
auto layout = qvLogDocument->documentLayout();
93-
// Anchor to the block at the top of the viewport; if the append
94-
// shifts its document-Y, replay the original sub-block offset.
95-
QTextBlock anchorBlock = ui->masterLogBrowser->cursorForPosition(QPoint(0, 0)).block();
96-
int viewportOffset = bar->value() - static_cast<int>(layout->blockBoundingRect(anchorBlock).y());
97-
FastAppendTextDocument(trimmedBatch, qvLogDocument);
98-
if (anchorBlock.isValid()) {
99-
int newY = static_cast<int>(layout->blockBoundingRect(anchorBlock).y());
100-
bar->setValue(newY + viewportOffset);
101-
}
102-
}
103-
});
86+
const QString trimmedBatch = batchToPrint.trimmed();
87+
if (trimmedBatch.isEmpty()) continue;
88+
89+
bool needsPost;
90+
{
91+
QMutexLocker pendingLocker(&logPendingMutex);
92+
if (!logPendingText.isEmpty()) logPendingText += '\n';
93+
logPendingText += trimmedBatch;
94+
if (logPendingText.size() > MAX_PENDING_LOG_CHARS) {
95+
const auto cut = logPendingText.indexOf('\n', logPendingText.size() - MAX_PENDING_LOG_CHARS);
96+
logPendingText = cut < 0 ? QString() : logPendingText.mid(cut + 1);
97+
}
98+
needsPost = !logFlushScheduled;
99+
logFlushScheduled = true;
100+
}
101+
// At most one flush in flight; anything produced meanwhile is picked up by
102+
// the flush that is already pending, so the UI event queue cannot grow.
103+
if (needsPost) runOnUiThread([this] { flush_log_batch(); });
104+
}
105+
}
106+
107+
void MainWindow::flush_log_batch() {
108+
QString batch;
109+
{
110+
QMutexLocker pendingLocker(&logPendingMutex);
111+
batch.swap(logPendingText);
112+
logFlushScheduled = false;
113+
}
114+
if (batch.isEmpty()) return;
115+
116+
auto bar = ui->masterLogBrowser->verticalScrollBar();
117+
if (Configs::dataManager->settingsRepo->log_auto_scroll) {
118+
FastAppendTextDocument(batch, qvLogDocument);
119+
bar->setValue(bar->maximum());
120+
} else {
121+
auto layout = qvLogDocument->documentLayout();
122+
// Anchor to the block at the top of the viewport; if the append
123+
// shifts its document-Y, replay the original sub-block offset.
124+
QTextBlock anchorBlock = ui->masterLogBrowser->cursorForPosition(QPoint(0, 0)).block();
125+
int viewportOffset = bar->value() - static_cast<int>(layout->blockBoundingRect(anchorBlock).y());
126+
FastAppendTextDocument(batch, qvLogDocument);
127+
if (anchorBlock.isValid()) {
128+
int newY = static_cast<int>(layout->blockBoundingRect(anchorBlock).y());
129+
bar->setValue(newY + viewportOffset);
104130
}
105131
}
106132
}
@@ -144,6 +170,11 @@ void MainWindow::on_masterLogBrowser_customContextMenuRequested(const QPoint &po
144170
auto action_clear = new QAction(this);
145171
action_clear->setText(tr("Clear"));
146172
connect(action_clear, &QAction::triggered, this, [=,this] {
173+
{
174+
// Otherwise a flush already in flight repaints what was just cleared.
175+
QMutexLocker pendingLocker(&logPendingMutex);
176+
logPendingText.clear();
177+
}
147178
qvLogDocument->clear();
148179
ui->masterLogBrowser->clear();
149180
});

0 commit comments

Comments
 (0)