perf(ui-web): release message stores and gate diagnostics
This commit is contained in:
+67
-15
@@ -31,23 +31,40 @@
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})();
|
})();
|
||||||
window.__APP_VER = '20260721-v7';
|
window.__APP_VER = '20260721-v7';
|
||||||
|
// 流式逐 token 诊断默认关闭。逐 token 读取 DOM 高度/文本并写 console 会
|
||||||
|
// 触发强制布局和跨进程日志传输,长时间运行会明显拖慢渲染器;需要排查时
|
||||||
|
// 由 Python 显式调用 setStreamDiagnostics(true) 打开。
|
||||||
|
window.__STREAM_DIAG = false;
|
||||||
|
window.setStreamDiagnostics = function(enabled) {
|
||||||
|
window.__STREAM_DIAG = !!enabled;
|
||||||
|
if (window.__STREAM_DIAG && window.__startStreamHeartbeat) {
|
||||||
|
window.__startStreamHeartbeat();
|
||||||
|
}
|
||||||
|
};
|
||||||
console.log('[JS] ===== app.js 加载 ver=20260721-v7 =====');
|
console.log('[JS] ===== app.js 加载 ver=20260721-v7 =====');
|
||||||
// 公式渲染依赖本地 KaTeX(离线);此处确认资源加载结果,缺失时打印告警便于定位
|
// 公式渲染依赖本地 KaTeX(离线);此处确认资源加载结果,缺失时打印告警便于定位
|
||||||
console.log('[JS] KaTeX ' + (typeof katex !== 'undefined' ? katex.version + ' 就绪' : '缺失(公式将退化为纯文本)'));
|
console.log('[JS] KaTeX ' + (typeof katex !== 'undefined' ? katex.version + ' 就绪' : '缺失(公式将退化为纯文本)'));
|
||||||
// 渲染器主线程心跳:dt 异常大 = 渲染器被阻塞(截图/重绘/GPU 等)
|
// 渲染器主线程心跳:dt 异常大 = 渲染器被阻塞(截图/重绘/GPU 等)
|
||||||
(function() {
|
(function() {
|
||||||
var _hbLast = Date.now();
|
var _hbLast = Date.now();
|
||||||
setInterval(function() {
|
var _started = false;
|
||||||
|
function tick() {
|
||||||
var _now = Date.now();
|
var _now = Date.now();
|
||||||
var _dt = _now - _hbLast;
|
var _dt = _now - _hbLast;
|
||||||
_hbLast = _now;
|
_hbLast = _now;
|
||||||
if (_dt >= 1500) {
|
if (_dt >= 1500) {
|
||||||
console.log('[JS] 心跳 dt=' + _dt + 'ms (渲染器主线程曾卡顿)');
|
console.log('[JS] 心跳 dt=' + _dt + 'ms (渲染器主线程曾卡顿)');
|
||||||
}
|
}
|
||||||
}, 1000);
|
}
|
||||||
|
window.__startStreamHeartbeat = function() {
|
||||||
|
if (_started) return;
|
||||||
|
_started = true;
|
||||||
|
setInterval(tick, 1000);
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
window.__diag = { events: [], cap: 300, tokenN: 0, thinkN: 0 };
|
window.__diag = { events: [], cap: 300, tokenN: 0, thinkN: 0 };
|
||||||
function diagEvent(name, extra) {
|
function diagEvent(name, extra) {
|
||||||
|
if (!window.__STREAM_DIAG) return;
|
||||||
try {
|
try {
|
||||||
var d = window.__diag;
|
var d = window.__diag;
|
||||||
d.events.push({ t: Date.now() % 1000000, e: name, x: extra });
|
d.events.push({ t: Date.now() % 1000000, e: name, x: extra });
|
||||||
@@ -749,7 +766,8 @@ function createMessage(msgId, role, initialText, senderName, branchInfo) {
|
|||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
replyDiv.innerText = initialText;
|
replyDiv.innerText = initialText;
|
||||||
} else {
|
} else {
|
||||||
// 🆕 助手纯文本消息(无时间线历史路径)也包进 md-segment → 与时间线路径同样有浅灰气泡背景
|
// 🆕 助手纯文本消息(无时间线历史路径)也包进 md-segment,
|
||||||
|
// 与时间线路径保持相同的透明正文布局
|
||||||
var _tHtml = typeof marked !== 'undefined' ? safeHtml(marked.parse(initialText)) : safeHtml(initialText);
|
var _tHtml = typeof marked !== 'undefined' ? safeHtml(marked.parse(initialText)) : safeHtml(initialText);
|
||||||
replyDiv.innerHTML = '<div class="md-segment markdown-body">' + _tHtml + '</div>';
|
replyDiv.innerHTML = '<div class="md-segment markdown-body">' + _tHtml + '</div>';
|
||||||
}
|
}
|
||||||
@@ -1208,7 +1226,8 @@ function syncRenderThrottled(msgId, minGapMs) {
|
|||||||
// Qt 看门狗入口:强制渲染(幂等,尾部无变化时开销极小)
|
// Qt 看门狗入口:强制渲染(幂等,尾部无变化时开销极小)
|
||||||
function forceRenderNow(msgId) {
|
function forceRenderNow(msgId) {
|
||||||
var _frNow = Date.now();
|
var _frNow = Date.now();
|
||||||
if (!window.__frLast || _frNow - window.__frLast > 1000) {
|
if (window.__STREAM_DIAG &&
|
||||||
|
(!window.__frLast || _frNow - window.__frLast > 1000)) {
|
||||||
window.__frLast = _frNow;
|
window.__frLast = _frNow;
|
||||||
console.log('[JS] forceRenderNow id=' + msgId + ' (看门狗)');
|
console.log('[JS] forceRenderNow id=' + msgId + ' (看门狗)');
|
||||||
}
|
}
|
||||||
@@ -1236,7 +1255,7 @@ function _fullRenderSegment(el, c, isThink) {
|
|||||||
}
|
}
|
||||||
function doStreamingRender(msgId) {
|
function doStreamingRender(msgId) {
|
||||||
window.__renderN = (window.__renderN || 0) + 1;
|
window.__renderN = (window.__renderN || 0) + 1;
|
||||||
if (window.__renderN % 10 === 1) {
|
if (window.__STREAM_DIAG && window.__renderN % 10 === 1) {
|
||||||
console.log('[JS] render#' + window.__renderN + ' 开始 id=' + msgId);
|
console.log('[JS] render#' + window.__renderN + ' 开始 id=' + msgId);
|
||||||
}
|
}
|
||||||
var buf = messageBuffer[msgId];
|
var buf = messageBuffer[msgId];
|
||||||
@@ -1288,10 +1307,12 @@ function appendReasoning(msgId, token) {
|
|||||||
buf.reasoning += token;
|
buf.reasoning += token;
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (!wrapper) { diagEvent('appendReasoning', 'NO_WRAPPER:' + msgId); return; }
|
if (!wrapper) { diagEvent('appendReasoning', 'NO_WRAPPER:' + msgId); return; }
|
||||||
|
if (window.__STREAM_DIAG) {
|
||||||
window.__diag.thinkN++;
|
window.__diag.thinkN++;
|
||||||
if (window.__diag.thinkN === 1 || window.__diag.thinkN % 25 === 0) {
|
if (window.__diag.thinkN === 1 || window.__diag.thinkN % 25 === 0) {
|
||||||
diagEvent('appendReasoning', 'n=' + window.__diag.thinkN);
|
diagEvent('appendReasoning', 'n=' + window.__diag.thinkN);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
wrapper.classList.add('streaming');
|
wrapper.classList.add('streaming');
|
||||||
var tl = ensureTimeline(wrapper, buf);
|
var tl = ensureTimeline(wrapper, buf);
|
||||||
if (!tl) return;
|
if (!tl) return;
|
||||||
@@ -1308,7 +1329,11 @@ function appendReasoning(msgId, token) {
|
|||||||
buf.thinkSegs.push(tc);
|
buf.thinkSegs.push(tc);
|
||||||
}
|
}
|
||||||
tc.__buf = (tc.__buf || '') + token;
|
tc.__buf = (tc.__buf || '') + token;
|
||||||
console.log('[JS] think#' + window.__diag.thinkN + ' 段buf=' + tc.__buf.length + 'c dom=' + (tc.textContent || '').length + 'c h=' + tc.offsetHeight);
|
if (window.__STREAM_DIAG) {
|
||||||
|
console.log('[JS] think#' + window.__diag.thinkN +
|
||||||
|
' 段buf=' + tc.__buf.length + 'c dom=' +
|
||||||
|
(tc.textContent || '').length + 'c h=' + tc.offsetHeight);
|
||||||
|
}
|
||||||
syncRenderThrottled(msgId); // ★ 同步通道
|
syncRenderThrottled(msgId); // ★ 同步通道
|
||||||
scheduleStreamingRender(msgId);
|
scheduleStreamingRender(msgId);
|
||||||
}
|
}
|
||||||
@@ -1319,10 +1344,12 @@ function appendToken(msgId, token) {
|
|||||||
buf.content += token;
|
buf.content += token;
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (!wrapper) { diagEvent('appendToken', 'NO_WRAPPER:' + msgId); return; }
|
if (!wrapper) { diagEvent('appendToken', 'NO_WRAPPER:' + msgId); return; }
|
||||||
|
if (window.__STREAM_DIAG) {
|
||||||
window.__diag.tokenN++;
|
window.__diag.tokenN++;
|
||||||
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
||||||
diagEvent('appendToken', 'n=' + window.__diag.tokenN);
|
diagEvent('appendToken', 'n=' + window.__diag.tokenN);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
wrapper.classList.add('streaming');
|
wrapper.classList.add('streaming');
|
||||||
var tl = ensureTimeline(wrapper, buf);
|
var tl = ensureTimeline(wrapper, buf);
|
||||||
if (!tl) return;
|
if (!tl) return;
|
||||||
@@ -1345,10 +1372,15 @@ function appendToken(msgId, token) {
|
|||||||
} else {
|
} else {
|
||||||
seg.__buf = (seg.__buf || '') + token;
|
seg.__buf = (seg.__buf || '') + token;
|
||||||
}
|
}
|
||||||
console.log('[JS] token#' + window.__diag.tokenN + ' 段buf=' + seg.__buf.length + 'c dom=' + (seg.textContent || '').length + 'c 段数=' + buf.textSegs.length + ' h=' + seg.offsetHeight);
|
if (window.__STREAM_DIAG) {
|
||||||
|
console.log('[JS] token#' + window.__diag.tokenN +
|
||||||
|
' 段buf=' + seg.__buf.length + 'c dom=' +
|
||||||
|
(seg.textContent || '').length + 'c 段数=' +
|
||||||
|
buf.textSegs.length + ' h=' + seg.offsetHeight);
|
||||||
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) {
|
||||||
diagEvent('tokenDOM', { buf: seg.__buf.length, dom: (seg.textContent || '').length });
|
diagEvent('tokenDOM', { buf: seg.__buf.length, dom: (seg.textContent || '').length });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
syncRenderThrottled(msgId); // ★ 同步通道:token 到 → 内容必现
|
syncRenderThrottled(msgId); // ★ 同步通道:token 到 → 内容必现
|
||||||
scheduleStreamingRender(msgId); // rAF 通道:更平滑(环境允许时)
|
scheduleStreamingRender(msgId); // rAF 通道:更平滑(环境允许时)
|
||||||
}
|
}
|
||||||
@@ -1357,7 +1389,12 @@ function appendToken(msgId, token) {
|
|||||||
function finishMessage(msgId) {
|
function finishMessage(msgId) {
|
||||||
console.log('[JS] finishMessage id=' + msgId);
|
console.log('[JS] finishMessage id=' + msgId);
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (!wrapper) { delete messageBuffer[msgId]; diagEvent('finish', 'NO_WRAPPER:' + msgId); return; }
|
if (!wrapper) {
|
||||||
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
|
diagEvent('finish', 'NO_WRAPPER:' + msgId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
diagEvent('finish', { id: msgId });
|
diagEvent('finish', { id: msgId });
|
||||||
var buf = messageBuffer[msgId];
|
var buf = messageBuffer[msgId];
|
||||||
cancelStreamingRender(msgId);
|
cancelStreamingRender(msgId);
|
||||||
@@ -1440,6 +1477,7 @@ function finishMessage(msgId) {
|
|||||||
if (buf.raf) { cancelAnimationFrame(buf.raf); buf.raf = 0; }
|
if (buf.raf) { cancelAnimationFrame(buf.raf); buf.raf = 0; }
|
||||||
}
|
}
|
||||||
delete messageBuffer[msgId];
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
|
|
||||||
// --- H. 刷新自定义滚动条 ---
|
// --- H. 刷新自定义滚动条 ---
|
||||||
reportWebScroll();
|
reportWebScroll();
|
||||||
@@ -1940,6 +1978,7 @@ function renderTimelineHistory(msgId, timelineJson) {
|
|||||||
// 删除 buffer → finishMessage 不再全量重渲(避免破坏时间线 DOM),
|
// 删除 buffer → finishMessage 不再全量重渲(避免破坏时间线 DOM),
|
||||||
// 但仍会执行代码高亮/滚动等收尾
|
// 但仍会执行代码高亮/滚动等收尾
|
||||||
delete messageBuffer[msgId];
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[JS]: renderTimelineHistory 解析失败', e);
|
console.error('[JS]: renderTimelineHistory 解析失败', e);
|
||||||
@@ -2063,6 +2102,23 @@ function hideLoadingOverlay() {
|
|||||||
setTimeout(function() { el.style.display = 'none'; }, 480);
|
setTimeout(function() { el.style.display = 'none'; }, 480);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 消息从当前窗口移除时一并释放附件缓存。附件使用 msgId-att-N 作为键,
|
||||||
|
// 只删除父消息键会让长文本和元数据跨会话持续留在内存中。
|
||||||
|
function clearMessageStores(msgId) {
|
||||||
|
if (!msgId) return;
|
||||||
|
delete messageBuffer[msgId];
|
||||||
|
delete __lastSyncRender[msgId];
|
||||||
|
delete longTextStore[msgId];
|
||||||
|
delete finalContentStore[msgId];
|
||||||
|
delete attachmentMetaStore[msgId];
|
||||||
|
var prefix = msgId + '-att-';
|
||||||
|
[longTextStore, attachmentMetaStore].forEach(function(store) {
|
||||||
|
Object.keys(store).forEach(function(key) {
|
||||||
|
if (key.indexOf(prefix) === 0) delete store[key];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 历史记录与视图控制 ====================
|
// ==================== 历史记录与视图控制 ====================
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
var bubbles = chatContainer.querySelectorAll('.message-wrapper, .system-note');
|
var bubbles = chatContainer.querySelectorAll('.message-wrapper, .system-note');
|
||||||
@@ -2073,6 +2129,7 @@ function clearChat() {
|
|||||||
longTextStore = {};
|
longTextStore = {};
|
||||||
finalContentStore = {};
|
finalContentStore = {};
|
||||||
attachmentMetaStore = {};
|
attachmentMetaStore = {};
|
||||||
|
__lastSyncRender = {};
|
||||||
|
|
||||||
// 🆕 P1-01:清渲染窗口状态机(游标/缓存/未决请求/代次;配置模式与大小保留)
|
// 🆕 P1-01:清渲染窗口状态机(游标/缓存/未决请求/代次;配置模式与大小保留)
|
||||||
if (typeof rwState !== 'undefined' && rwState && typeof RenderWindowState !== 'undefined') {
|
if (typeof rwState !== 'undefined' && rwState && typeof RenderWindowState !== 'undefined') {
|
||||||
@@ -2093,9 +2150,7 @@ function deleteMessage(msgId) {
|
|||||||
wrapper.style.transform = "translateY(-10px)";
|
wrapper.style.transform = "translateY(-10px)";
|
||||||
setTimeout(function() { wrapper.remove(); }, 300);
|
setTimeout(function() { wrapper.remove(); }, 300);
|
||||||
}
|
}
|
||||||
delete messageBuffer[msgId];
|
clearMessageStores(msgId);
|
||||||
delete longTextStore[msgId];
|
|
||||||
delete finalContentStore[msgId];
|
|
||||||
setTimeout(reportWebScroll, 350);
|
setTimeout(reportWebScroll, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2267,10 +2322,7 @@ function rwCaptureAnchor() {
|
|||||||
function rwRemoveMessageDom(msgId) {
|
function rwRemoveMessageDom(msgId) {
|
||||||
var wrapper = document.getElementById(msgId);
|
var wrapper = document.getElementById(msgId);
|
||||||
if (wrapper) wrapper.remove();
|
if (wrapper) wrapper.remove();
|
||||||
delete messageBuffer[msgId];
|
clearMessageStores(msgId);
|
||||||
delete longTextStore[msgId];
|
|
||||||
delete finalContentStore[msgId];
|
|
||||||
delete attachmentMetaStore[msgId];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载入口:固定在 chat-container 首/尾;无更多消息时隐藏。 */
|
/* 加载入口:固定在 chat-container 首/尾;无更多消息时隐藏。 */
|
||||||
|
|||||||
Reference in New Issue
Block a user