Skip to content

Commit d3fc68c

Browse files
committed
fix(gui): resolve session slowdown and hang under sustained compare load
A long GUI session could degrade until the app needed to be force-quit. Root cause was runaway /progress polling on the Webpage and Document compare pages: if a compare response was ever lost, the 200ms poller fired indefinitely and saturated the bridge's 16-thread worker pool, 503-ing every request including /cancel. - Cap progress polling on WebpageComparePage and DocumentComparePage (progressPollMax ~5min); on timeout, cancel the wedged compare on the bridge and recover the UI. Mirror the same cancel-on-timeout in Main.qml's progress cap. - Bound log file growth: rotate linsync.log to a single .1 backup past 8 MiB at startup (logging.rs). - Release the global bridge state lock during /save file I/O via a three-phase prepare/perform/finish split, with a re-lock race guard so a concurrent edit is never silently marked saved. - Keep /cancel and /progress servicable under saturation: route them before the worker-cap check (spawn-first, request-line routing) so the UI can always recover an in-flight compare. - Bump workspace version to 1.16.3 and sync packaging recipes, changelogs, and metainfo.
1 parent ddfa8aa commit d3fc68c

13 files changed

Lines changed: 705 additions & 144 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ members = [
99
resolver = "3"
1010

1111
[workspace.package]
12-
version = "1.16.2"
12+
version = "1.16.3"
1313
edition = "2024"
1414
license = "GPL-3.0-only"
1515
repository = "https://github.com/visorcraft/LinSync"

apps/linsync-gui/qml/DocumentComparePage.qml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ Controls.Pane {
4747
property int progressCurrent: 0
4848
property int progressTotal: 0
4949
property string progressMessage: ""
50+
// Defensive iteration cap for the progress timer so a compare whose XHR
51+
// never completes (bridge worker wedged, OCR plugin hang, lost response)
52+
// cannot poll /progress forever and saturate the bridge connection pool.
53+
// At 200 ms intervals, 1500 iterations == 5 minutes. Mirrors Main.qml's
54+
// progressPollMax safety net.
55+
property int progressPollCount: 0
56+
readonly property int progressPollMax: 1500
5057
property int selectedRenderedPageIndex: -1
5158
readonly property bool isRenderedMode: modeCombo.currentText === "Rendered"
5259
property real renderedZoom: 1.0
@@ -98,6 +105,7 @@ Controls.Pane {
98105
root.progressCurrent = 0;
99106
root.progressTotal = 0;
100107
root.progressMessage = "";
108+
root.progressPollCount = 0;
101109

102110
const modeStr = {
103111
"OCR Text": "ocr_text",
@@ -142,6 +150,23 @@ Controls.Pane {
142150
repeat: true
143151
running: root.running && root.activeRequestId !== ""
144152
onTriggered: {
153+
if (!root.running || root.activeRequestId === "") {
154+
progressTimer.stop()
155+
return
156+
}
157+
// If the compare XHR never reports back, stop polling, cancel the
158+
// request on the bridge (so its worker slot is freed), and recover
159+
// the UI instead of polling forever and exhausting the connection
160+
// pool — the "app gets slow until restarted" failure mode.
161+
root.progressPollCount += 1
162+
if (root.progressPollCount > root.progressPollMax) {
163+
root.bridgeGet("/cancel?id=" + encodeURIComponent(root.activeRequestId), function () {})
164+
root.running = false
165+
root.activeRequestId = ""
166+
root.statusText = qsTr("Compare timed out — no response from the bridge")
167+
progressTimer.stop()
168+
return
169+
}
145170
root.bridgeGet("/progress?id=" + encodeURIComponent(root.activeRequestId), function (ok, data) {
146171
if (!ok || !data)
147172
return;

apps/linsync-gui/qml/Main.qml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6825,6 +6825,14 @@ Kirigami.ApplicationWindow {
68256825
}
68266826
root.progressPollCount += 1
68276827
if (root.progressPollCount > root.progressPollMax) {
6828+
// Compare never reported back. Cancel it on the bridge so its
6829+
// worker slot is freed (cooperative cancel — polled by the
6830+
// folder/text/image compare loops), then recover the UI
6831+
// instead of polling forever and exhausting the connection
6832+
// pool — the "app gets slow until restarted" failure mode.
6833+
var hungId = root.activeRequestId
6834+
if (hungId && root.bridgeUrl)
6835+
root.bridgeGet("/cancel?id=" + encodeURIComponent(hungId), function () {})
68286836
root.comparing = false
68296837
root.activeRequestId = ""
68306838
root.statusText = qsTr("Compare monitoring timed out")

apps/linsync-gui/qml/WebpageComparePage.qml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ Controls.Pane {
6565
property int progressTotal: 0
6666
property string progressMessage: ""
6767
property bool pendingNewTab: false
68+
// Defensive iteration cap for the progress timer so a compare whose XHR
69+
// never completes (bridge worker wedged, plugin hang, lost response)
70+
// cannot poll /progress forever and saturate the bridge connection pool.
71+
// At 200 ms intervals, 1500 iterations == 5 minutes. Mirrors Main.qml's
72+
// progressPollMax safety net.
73+
property int progressPollCount: 0
74+
readonly property int progressPollMax: 1500
6875

6976
// Per-row diff tints over the page background (left=red, right=green,
7077
// changed=amber); "equal" rows are absent → transparent.
@@ -131,6 +138,7 @@ Controls.Pane {
131138
root.progressCurrent = 0;
132139
root.progressTotal = 0;
133140
root.progressMessage = "";
141+
root.progressPollCount = 0;
134142
const left = encodeURIComponent(root.leftUrl);
135143
const right = encodeURIComponent(root.rightUrl);
136144
const mode = encodeURIComponent(root.subMode);
@@ -196,6 +204,25 @@ Controls.Pane {
196204
repeat: true
197205
running: root.busy && root.activeRequestId !== ""
198206
onTriggered: {
207+
if (!root.busy || root.activeRequestId === "") {
208+
progressTimer.stop()
209+
return
210+
}
211+
// If the compare XHR never reports back, stop polling, cancel the
212+
// request on the bridge (so its worker slot is freed), and recover
213+
// the UI instead of polling forever and exhausting the connection
214+
// pool — the "app gets slow until restarted" failure mode.
215+
root.progressPollCount += 1
216+
if (root.progressPollCount > root.progressPollMax) {
217+
root.bridgeGet("/cancel?id=" + encodeURIComponent(root.activeRequestId), function () {})
218+
root.busy = false
219+
root.activeRequestId = ""
220+
root.pendingNewTab = false
221+
root.resultError = true
222+
root.resultSummary = qsTr("Compare timed out — no response from the bridge")
223+
progressTimer.stop()
224+
return
225+
}
199226
root.bridgeGet("/progress?id=" + encodeURIComponent(root.activeRequestId), function (ok, data) {
200227
if (!ok || !data)
201228
return;

0 commit comments

Comments
 (0)