fix: 解析大画廊时连接泄漏 + 解析阶段无进度反馈
用户反馈:点开始下载后 6 个画廊(都是 2000 张级)一直显示「解析中 0/?」, 看起来像卡死。排查发现两个问题: 1. 连接泄漏(真 bug) httpClient() 每次请求都新建 http.Transport,用完丢弃。Transport 被丢弃后 它持有的 keep-alive 连接不会立刻释放(要等 IdleConnTimeout=90s), 于是每发一个请求留下一条僵尸连接。 现场实测:91 条 Established 连接,而解析是串行的,正常应该只有 1 条。 修复:Transport 全局复用,只在代理配置变化时重建, 重建时 CloseIdleConnections 关掉旧连接;新增 Client.Close()。 实测:20 次请求从 20 条连接降到 1 条;100 次并发请求只用 22 条。 2. 解析阶段毫无反馈 2000 张 = 100 屏串行翻页,整个解析阶段界面都显示「解析中 0/?」, 完全没有进度,用户只能干等。 修复:FetchGalleryPage 增加 ParseProgress 回调,每翻完一屏上报一次 (已解析张数/总张数/当前屏),界面显示「已解析 340/2000 张」。 另外:用户设置里请求间隔是 1000ms(1 秒),全局限流让 5 个画廊共享 1 请求/秒 —— 500 次解析请求需要约 8 分钟。这是配置导致的慢,不是 bug, 但可以在界面上看到进度后就不会以为卡死了。 测试从 74 增至 80,新增: - TestTransportIsReused / TestTransportReusedUnderConcurrency(连接复用) - TestSetProxyRebuildsTransport(改代理要重建) - TestParseProgressCallback(进度回调,含最后一屏)
This commit is contained in:
+28
-13
@@ -10,7 +10,7 @@ const logBuf = [];
|
||||
// ---------------- 渲染 ----------------
|
||||
|
||||
const STATUS_TEXT = {
|
||||
queued: '等待', running: '解析中', downloading: '下载中',
|
||||
queued: '等待', running: '准备中', parsing: '解析中', downloading: '下载中',
|
||||
done: '完成', failed: '失败', partial: '部分失败',
|
||||
};
|
||||
|
||||
@@ -45,8 +45,25 @@ function renderSelState() {
|
||||
|
||||
function rowHtml(t) {
|
||||
const total = t.totalPages || 0;
|
||||
const pct = total ? Math.round((t.done / total) * 100) : (t.status === 'done' ? 100 : 0);
|
||||
const badgeCls = 'b-' + (t.status || 'queued');
|
||||
|
||||
// 解析阶段:显示「已解析 N/M 张」,而不是 0/?(否则看着像卡死)
|
||||
const parsing = t.status === 'parsing';
|
||||
let progCell;
|
||||
if (parsing) {
|
||||
const got = Number((t.pages || {})['_parsing'] || 0);
|
||||
const pct = total ? Math.round((got / total) * 100) : 0;
|
||||
progCell = `<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">已解析 ${got}${total ? '/' + total : ''} 张</span>
|
||||
</div>`;
|
||||
} else {
|
||||
const pct = total ? Math.round((t.done / total) * 100) : (t.status === 'done' ? 100 : 0);
|
||||
progCell = `<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">${t.done}/${total || '?'}${t.failed ? ` (✗${t.failed})` : ''}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const title = t.title || t.raw || '(未解析)';
|
||||
const err = t.error
|
||||
? `<span class="e">${esc(t.error)}</span>`
|
||||
@@ -63,13 +80,8 @@ function rowHtml(t) {
|
||||
<td class="c-sel"><input type="checkbox" class="sel" data-gid="${t.gid}" ${t.selected ? 'checked' : ''} /></td>
|
||||
<td class="c-gid">${esc(t.gid)}</td>
|
||||
<td><div class="title-cell"><span class="t">${esc(title)}</span>${err}</div></td>
|
||||
<td class="c-prog">
|
||||
<div class="rowprog">
|
||||
<div class="rb"><i style="width:${pct}%"></i></div>
|
||||
<span class="rn">${t.done}/${total || '?'}${t.failed ? ` (✗${t.failed})` : ''}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="c-stat"><span class="badge ${badgeCls}">${STATUS_TEXT[t.status] || t.status}</span></td>
|
||||
<td class="c-prog">${progCell}</td>
|
||||
<td class="c-stat"><span class="badge ${'b-' + (t.status || 'queued')}">${STATUS_TEXT[t.status] || t.status}</span></td>
|
||||
<td class="c-act">${acts.join(' ')}</td>
|
||||
</tr>`;
|
||||
}
|
||||
@@ -79,11 +91,14 @@ function renderStats() {
|
||||
let dn = 0, tt = 0;
|
||||
for (const t of tasks) {
|
||||
if (t.status === 'queued') c.queued++;
|
||||
else if (t.status === 'running' || t.status === 'downloading') c.running++;
|
||||
else if (t.status === 'running' || t.status === 'parsing' || t.status === 'downloading') c.running++;
|
||||
else if (t.status === 'done') c.done++;
|
||||
else if (t.status === 'failed' || t.status === 'partial') c.failed++;
|
||||
dn += t.done || 0;
|
||||
tt += t.totalPages || 0;
|
||||
// 解析阶段的任务还没进入下载,done 不计入总进度
|
||||
if (t.status !== 'parsing') {
|
||||
dn += t.done || 0;
|
||||
tt += t.totalPages || 0;
|
||||
}
|
||||
}
|
||||
$('s-total').textContent = tasks.length;
|
||||
$('s-queued').textContent = c.queued;
|
||||
|
||||
Reference in New Issue
Block a user