From 56b6bdd1b473120045679381b5441a2332cf8493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E4=B8=80=E4=B8=81?= <1769123563@qq.com> Date: Sat, 22 Nov 2025 01:48:39 +0800 Subject: [PATCH] Enhance the Performance of Frontend Log Output --- templates/index.html | 1579 +++++++----------------------------------- 1 file changed, 241 insertions(+), 1338 deletions(-) diff --git a/templates/index.html b/templates/index.html index 07eaa1d..68e547f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1505,486 +1505,38 @@ }); } - // 【Phase 2.2新增】Layout属性缓存类 - 减少强制重排70% - class LayoutCache { - constructor(element) { - this.element = element; - this.cache = {}; - this.cacheTime = 0; - this.cacheDuration = 16; // 一帧内(16ms)缓存有效 - } - - get(property) { - const now = performance.now(); - // 超过一帧时间,重新读取 - if (now - this.cacheTime > this.cacheDuration) { - // 批量读取所有Layout属性,一帧内只读一次 - this.cache = { - scrollTop: this.element.scrollTop, - scrollHeight: this.element.scrollHeight, - clientHeight: this.element.clientHeight, - offsetHeight: this.element.offsetHeight, - scrollWidth: this.element.scrollWidth, - clientWidth: this.element.clientWidth - }; - this.cacheTime = now; - } - return this.cache[property]; - } - - // 清除缓存,强制下次读取 - invalidate() { - this.cacheTime = 0; - } - } - - // 高性能日志虚拟渲染器:双缓冲 + 分帧渲染 + 智能批处理 + // 简化日志渲染器:实时逐行追加,不做虚拟截断 class LogVirtualList { constructor(container) { this.container = container; - // 【图层优化】scrollElement 就是 container(.console-layer),因为每个图层独立滚动 - this.scrollElement = container; this.lines = []; - this.pending = []; - this.pool = []; - this.lineHeight = 18; - - // 【Phase 1.3优化】大幅增加内存限制,用内存换速度 - this.maxVisible = 150; // ↑ from 120(增加可见行数) - this.maxLines = 50000; // ↑ from 10000(5倍提升,约5MB/app) - this.trimTarget = 40000; // ↑ from 8000(保留更多历史) - this.preRenderBuffer = 90; // 新增:上下各预渲染90行,提升滚动流畅度 - this.minPoolSize = 220; // 保底DOM池,避免窗口渲染空白 - this.poolHardLimit = 800; // 池子硬上限,防止撑爆内存 - this.maxPoolSize = Math.max( - this.minPoolSize, - this.maxVisible + this.preRenderBuffer * 2 + 50 - ); // 默认覆盖可视窗口+缓冲区,再留一些余量 - this.rafId = null; + this.isActive = false; this.autoScrollEnabled = true; + this.needsScroll = false; this.resumeDelay = 3000; this.resumeTimer = null; - this.flushTimer = null; - this.lastRenderHash = null; - this.scrollLocked = false; - this.needsScroll = false; - this.scrollHandler = null; - this.beforeSpacer = null; - this.afterSpacer = null; - this.watchdogTimer = null; - this.lastRenderAt = 0; - this.idleTimer = null; - this.idleTimeout = 3000; // 用户3秒不滚动后自动吸附 - - // 【优化吸附逻辑】记录用户滚动位置,用于智能恢复 - this.lastUserScrollPosition = 0; // 用户上次滚动的位置 - this.userScrollDistance = 0; // 用户向上滚动的距离 - - // 【优化速度】批处理参数 - 快速响应优先 - this.batchThreshold = 20; // ↓ from 30(更快触发,减少延迟) - this.batchDelay = 30; // ↓ from 50(加快响应速度) - this.lastFlushTime = 0; + this.renderScheduled = false; + this.lastRenderTime = 0; + this.lastRenderLineCount = 0; + this.pendingHighWaterMark = 0; this.flushCount = 0; - - // 【Phase 1.2新增】动态自适应参数(保留供将来使用) - this.minBatchThreshold = 10; // 低负载:极致实时 - this.maxBatchThreshold = 100; // 高负载:批量优化 - this.minDelay = 10; // CPU空闲时 - this.baseDelay = 30; // 正常情况 - this.maxDelay = 100; // CPU繁忙时(降低) - this.cpuIdleCallback = null; // CPU负载检测回调 - - // 【Phase 1.4优化】分帧渲染参数 - 用内存换速度 - this.renderBatchSize = 500; // ↑ from 300(每帧渲染更多行,减少分帧次数) - this.renderQueue = []; // 待渲染队列 - this.isRendering = false; // 是否正在分帧渲染 - - // 【新增】性能监控 - this.pendingHighWaterMark = 0; // pending队列最大值,用于调试 - this.renderTime = 0; // 渲染耗时 - this.lastRenderLineCount = 0; // 上次渲染的行数 - - // 【图层优化】窗口激活状态管理 - this.isActive = false; // 当前窗口是否为活动窗口 - this.needsRender = false; // 非活动窗口是否有待渲染内容 - - // 【修复黑屏】添加渲染锁,防止并发渲染导致黑屏 - this.isRendering = false; // 是否正在渲染 - this.renderPending = false; // 渲染期间是否有新的渲染请求 - - // 【Phase 2.2新增】初始化Layout缓存,减少强制重排 - this.layoutCache = null; // 延迟初始化,等scrollElement准备好 - - // 【状态栏优化】引用全局状态栏元素,用于显示系统消息而不干扰日志 - this.statusBar = document.getElementById('consoleStatusBar'); - this.statusBarTimer = null; // 状态栏自动隐藏定时器 - - this.attachScroll(); - - // 【Phase 2.2】初始化Layout缓存 - if (this.scrollElement) { - this.layoutCache = new LayoutCache(this.scrollElement); - } - } - - clearIdleTimer() { - if (this.idleTimer) { - clearTimeout(this.idleTimer); - this.idleTimer = null; - } - } - - startIdleTimer() { - this.clearIdleTimer(); - this.idleTimer = setTimeout(() => { - this.autoScrollEnabled = true; - this.needsScroll = true; - this.clearResumeTimer(); - this.scrollToBottom(); - }, this.idleTimeout); - } - - // 保障DOM池容量,避免可视窗口比池子大导致空白 - ensurePoolCapacity(neededCount) { - if (!Number.isFinite(neededCount)) return; - - // 根据需求量和基线计算DOM池大小,避免窗口比池子大 - const baseline = Math.min(this.minPoolSize, neededCount + 80); - const targetSize = Math.min( - Math.max(neededCount, baseline, this.pool.length), - this.poolHardLimit - ); - - if (targetSize > this.maxPoolSize) { - this.maxPoolSize = targetSize; - } - - const missing = Math.max(0, targetSize - this.pool.length); - for (let i = 0; i < missing; i++) { - const node = document.createElement('div'); - node.className = 'console-line'; - this.pool.push(node); - } - } - - // DOM意外空白时强制刷新 - forceRenderIfBlank() { - if (!this.container) return false; - if (this.lines.length === 0) return false; - if (this.container.querySelector('.console-line')) return false; this.lastRenderHash = null; - this.needsScroll = true; - this.scheduleRender(true); - return true; - } - - startWatchdog() { - if (this.watchdogTimer) return; - this.watchdogTimer = setInterval(() => { - if (!this.container) return; - // 黑屏兜底:有数据但没有节点时强制渲染 - const hasLines = this.lines.length > 0; - const hasNodes = !!this.container.querySelector('.console-line'); - if (hasLines && !hasNodes && !this.isRendering) { - this.lastRenderHash = null; - this.needsScroll = true; - this.updateStatusBar('正在恢复显示...', '检测到渲染延迟', false, 1200); - this.scheduleRender(true); - return; - } - - // 渲染长时间未推进时,提醒并触发一次刷新 - const now = performance.now(); - if (hasLines && this.pending.length > 0 && now - this.lastRenderAt > 1500 && !this.isRendering) { - this.lastRenderHash = null; - this.needsScroll = true; - this.updateStatusBar('日志渲染中...', `${this.pending.length} 条待显示`, false, 1200); - this.scheduleRender(true); - } - }, 800); - } - - stopWatchdog() { - if (this.watchdogTimer) { - clearInterval(this.watchdogTimer); - this.watchdogTimer = null; - } - } - - attachScroll() { - if (!this.scrollElement) return; - if (this.scrollHandler) return; // 防止重复绑定 - - let scrollTimer = null; - this.scrollHandler = () => { - // 【优化滚动体验】防抖处理,避免频繁触发 - if (scrollTimer) clearTimeout(scrollTimer); - scrollTimer = setTimeout(() => { - this.handleUserScroll(); - }, 50); // 优化:50ms防抖,快速响应用户滚动 - }; - - this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true }); - } - - // 【新增】性能统计方法 - getPerformanceStats() { - return { - totalLines: this.lines.length, - pendingLines: this.pending.length, - pendingHighWaterMark: this.pendingHighWaterMark, - flushCount: this.flushCount, - lastRenderTime: this.renderTime.toFixed(2) + 'ms', - lastRenderLineCount: this.lastRenderLineCount, - poolSize: this.pool.length, - memoryEstimate: this.estimateMemoryUsage() - }; - } - - // 【新增】估算内存使用 - estimateMemoryUsage() { - // 粗略估算:每行平均100字节(文本+对象开销) - const linesMemory = this.lines.length * 100; - const pendingMemory = this.pending.length * 100; - const poolMemory = this.pool.length * 500; // DOM节点更大 - const totalBytes = linesMemory + pendingMemory + poolMemory; - - if (totalBytes < 1024) { - return totalBytes + ' B'; - } else if (totalBytes < 1024 * 1024) { - return (totalBytes / 1024).toFixed(2) + ' KB'; - } else { - return (totalBytes / 1024 / 1024).toFixed(2) + ' MB'; - } - } - - // 【新增】重置性能统计 - resetPerformanceStats() { - this.pendingHighWaterMark = this.pending.length; - this.flushCount = 0; - this.renderTime = 0; - console.log('[性能统计] 已重置性能计数器'); - } - - // 【状态栏优化】更新状态栏消息,而不干扰日志查看 - updateStatusBar(message, details = '', autoHide = true, duration = 3000) { - // 只有当前活动的engine才能更新状态栏 - if (!this.isActive || !this.statusBar) return; - - const messageElem = this.statusBar.querySelector('.status-message'); - const detailsElem = this.statusBar.querySelector('.status-details'); - - if (messageElem) messageElem.textContent = message; - if (detailsElem) detailsElem.textContent = details; - - // 显示状态栏 - this.statusBar.classList.add('visible'); - if (this.layoutCache) { - this.layoutCache.invalidate(); - } - - // 清除之前的自动隐藏定时器 - if (this.statusBarTimer) { - clearTimeout(this.statusBarTimer); - this.statusBarTimer = null; - } - - // 自动隐藏 - if (autoHide && duration > 0) { - this.statusBarTimer = setTimeout(() => { - if (this.statusBar) { - this.statusBar.classList.remove('visible'); - } - if (this.layoutCache) { - this.layoutCache.invalidate(); - } - this.statusBarTimer = null; - }, duration); - } - } - - // 【状态栏优化】隐藏状态栏 - hideStatusBar() { - if (!this.isActive || !this.statusBar) return; - if (this.statusBarTimer) { - clearTimeout(this.statusBarTimer); - this.statusBarTimer = null; - } - this.statusBar.classList.remove('visible'); - if (this.layoutCache) { - this.layoutCache.invalidate(); - } - } - - // 添加清理方法 - dispose() { - console.log('[资源清理] 开始清理LogVirtualList资源...'); - console.log('[性能统计] 最终统计:', this.getPerformanceStats()); - - // 清理定时器 - this.stopWatchdog(); - if (this.rafId) { - cancelAnimationFrame(this.rafId); - this.rafId = null; - } - this.clearResumeTimer(); - if (this.flushTimer) { - clearTimeout(this.flushTimer); - this.flushTimer = null; - } - this.clearIdleTimer(); - if (this.statusBarTimer) { - clearTimeout(this.statusBarTimer); - this.statusBarTimer = null; - } - - // 移除事件监听器 - if (this.scrollElement && this.scrollHandler) { - this.scrollElement.removeEventListener('scroll', this.scrollHandler); - this.scrollHandler = null; - } - - // 【优化】清空数据结构,释放内存 - this.lines.length = 0; - this.pending.length = 0; - - // 【优化】清空并释放DOM节点池 - this.pool.forEach(node => { - if (node && node.parentNode) { - node.parentNode.removeChild(node); - } - }); - this.pool.length = 0; - - // 清理占位符 - if (this.beforeSpacer && this.beforeSpacer.parentNode) { - this.beforeSpacer.parentNode.removeChild(this.beforeSpacer); - } - if (this.afterSpacer && this.afterSpacer.parentNode) { - this.afterSpacer.parentNode.removeChild(this.afterSpacer); - } - this.beforeSpacer = null; - this.afterSpacer = null; - - // 清空容器 + this.renderPending = false; + this.maxLines = Number.MAX_SAFE_INTEGER; + this.trimTarget = Number.MAX_SAFE_INTEGER; + this.scrollHandler = this.handleScroll.bind(this); if (this.container) { - this.container.innerHTML = ''; + this.container.addEventListener('scroll', this.scrollHandler, { passive: true }); } - - console.log('[资源清理] LogVirtualList资源清理完成'); } - /** - * 【优化滚动体验】处理用户手动滚动 - * - 用户滚动到底部:立即启用自动滚动 - * - 用户滚动离开底部:禁用自动滚动,智能计算恢复时间 - * - 智能恢复:滚动距离越大,恢复时间越长(最长5秒) - */ - handleUserScroll() { - if (!this.scrollElement || this.scrollLocked) return; - - // 获取当前滚动位置 - const currentScrollTop = this.layoutCache - ? this.layoutCache.get('scrollTop') - : this.scrollElement.scrollTop; - const scrollHeight = this.layoutCache - ? this.layoutCache.get('scrollHeight') - : this.scrollElement.scrollHeight; - const clientHeight = this.layoutCache - ? this.layoutCache.get('clientHeight') - : this.scrollElement.clientHeight; - - // 计算距离底部的距离 - const distanceFromBottom = scrollHeight - clientHeight - currentScrollTop; - const atBottom = distanceFromBottom <= 80; // 更宽松的阈值,减少误判 - - if (atBottom) { - // 用户主动滚动到底部,立即启用自动滚动 - if (!this.autoScrollEnabled) { - console.log('[自动吸附] 用户滚动到底部,立即启用自动滚动'); - } - this.autoScrollEnabled = true; - this.clearResumeTimer(); - this.userScrollDistance = 0; - this.lastUserScrollPosition = currentScrollTop; - return; + dispose() { + if (this.container && this.scrollHandler) { + this.container.removeEventListener('scroll', this.scrollHandler); } - - // 用户主动向上滚动查看历史 - // 计算用户向上滚动的距离 - if (this.lastUserScrollPosition > 0) { - const scrollDelta = this.lastUserScrollPosition - currentScrollTop; - if (scrollDelta > 0) { // 向上滚动 - this.userScrollDistance = Math.max(this.userScrollDistance, distanceFromBottom); - } - } - this.lastUserScrollPosition = currentScrollTop; - - // 如果自动滚动已经禁用,只更新计时器 - if (!this.autoScrollEnabled) { - this.clearResumeTimer(); - const delay = this.calculateResumeDelay(distanceFromBottom); - this.startResumeTimer(delay); - this.startIdleTimer(); - return; - } - - // 第一次离开底部,禁用自动滚动 - console.log(`[自动吸附] 用户离开底部 ${Math.round(distanceFromBottom)}px,禁用自动滚动`); - this.autoScrollEnabled = false; this.clearResumeTimer(); - this.clearIdleTimer(); - - // 智能计算恢复时间 - const delay = this.calculateResumeDelay(distanceFromBottom); - this.startResumeTimer(delay); - this.startIdleTimer(); - } - - /** - * 【智能恢复】根据滚动距离计算恢复延迟 - * - 距离底部 < 200px: 2秒恢复(用户只是稍微往上看) - * - 距离底部 200-500px: 3秒恢复(默认) - * - 距离底部 > 500px: 5秒恢复(用户在查看较早的历史) - */ - calculateResumeDelay(distanceFromBottom) { - if (distanceFromBottom < 200) { - return 2000; // 2秒快速恢复 - } else if (distanceFromBottom < 500) { - return 3000; // 默认3秒 - } else if (distanceFromBottom < 1000) { - return 4000; // 4秒 - } else { - return 5000; // 最长5秒 - } - } - - /** - * 【优化】启动恢复计时器,等待用户停止滚动后自动恢复吸附 - */ - startResumeTimer(delay) { - this.resumeTimer = setTimeout(() => { - console.log(`[自动吸附] ${delay}ms后恢复自动滚动,吸附到最新日志`); - this.autoScrollEnabled = true; - this.userScrollDistance = 0; - this.needsScroll = true; - - // 【优化】恢复时立即吸附到最新 - // 如果有新内容已经渲染完成,直接滚动到最新位置 - if (this.lines.length > 0) { - // 等待一帧,确保任何正在进行的渲染完成 - requestAnimationFrame(() => { - // 再次检查是否有正在渲染的内容 - if (this.isRendering) { - // 如果正在渲染,标记需要滚动,渲染完成后会自动滚动 - this.needsScroll = true; - } else { - // 没有正在渲染,直接滚动到最新 - this.scrollToBottom(); - } - }); - } - }, delay); + this.container = null; + this.lines = []; } clearResumeTimer() { @@ -1994,893 +1546,195 @@ } } + handleScroll() { + if (!this.container) return; + const distanceFromBottom = this.container.scrollHeight - this.container.clientHeight - this.container.scrollTop; + const atBottom = distanceFromBottom <= 8; + if (atBottom) { + this.autoScrollEnabled = true; + this.needsScroll = true; + this.clearResumeTimer(); + return; + } + + this.autoScrollEnabled = false; + this.clearResumeTimer(); + this.resumeTimer = setTimeout(() => { + this.autoScrollEnabled = true; + this.needsScroll = true; + this.scrollToLatest(true); + }, this.resumeDelay); + } + isNearBottom() { - if (!this.scrollElement) return true; - - // 【Phase 2.2优化】使用Layout缓存减少强制重排 - let scrollTop, clientHeight, scrollHeight; - - if (this.layoutCache) { - scrollTop = this.layoutCache.get('scrollTop'); - clientHeight = this.layoutCache.get('clientHeight'); - scrollHeight = this.layoutCache.get('scrollHeight'); - } else { - // 降级方案:直接读取(无缓存时) - scrollTop = this.scrollElement.scrollTop; - clientHeight = this.scrollElement.clientHeight; - scrollHeight = this.scrollElement.scrollHeight; - } - - // 【优化判断】智能阈值: - // - 超小内容时使用30px - // - 常规50px - // - 当存在状态条/顶部padding时,再增加20px缓冲 - const baseThreshold = scrollHeight < 1000 ? 30 : 50; - const threshold = baseThreshold + 20; - const distanceFromBottom = scrollHeight - clientHeight - scrollTop; - - return distanceFromBottom <= threshold; + if (!this.container) return true; + const distanceFromBottom = this.container.scrollHeight - this.container.clientHeight - this.container.scrollTop; + return distanceFromBottom <= 8; } - /** - * 【Phase 3新增】检测用户是否正在查看最新日志 - * 用于智能决策:用户关注时立即更新,后台时节省资源 - */ - isUserWatching() { - // 窗口必须是活动状态 - if (!this.isActive) return false; + scrollToLatest(force = false) { + if (!this.container) return; + if (!force && !this.autoScrollEnabled) return; - // 页面必须可见 - if (document.hidden) return false; + if (this.container.scrollHeight <= this.container.clientHeight) { + this.container.scrollTop = 0; + this.needsScroll = false; + return; + } - // 用户滚动位置必须在底部附近(正在查看最新日志) - return this.isNearBottom(); + const target = this.container.scrollHeight - this.container.clientHeight; + this.container.scrollTop = target; + // 双保险:下一帧再吸附一次,避免渲染时机导致未到达底部 + requestAnimationFrame(() => { + if (this.container) { + this.container.scrollTop = this.container.scrollHeight - this.container.clientHeight; + } + this.needsScroll = false; + }); } - // 将最新一行吸附到可视区域内,而不是盲目对齐到底部 - scrollLatestIntoView(margin = 12) { - if (!this.scrollElement || this.scrollLocked) return false; - const lastLine = this.scrollElement.querySelector('.console-line:last-of-type'); - if (!lastLine) return false; - - const container = this.scrollElement; - // 最新一行希望落在视口内并留出少量下边距 - const desiredBottom = container.clientHeight - margin; - const delta = lastLine.offsetTop + lastLine.offsetHeight - desiredBottom; - if (delta > 1 || delta < -1) { - this.scrollLocked = true; - requestAnimationFrame(() => { - container.scrollTop = Math.max(0, container.scrollTop + delta); - if (this.layoutCache) { - this.layoutCache.invalidate(); - } - setTimeout(() => { - this.scrollLocked = false; - }, 80); - }); - } - return true; + forceScrollToLatest() { + // 三次确认(当前帧、下一帧、50ms后),降低偶发现象 + this.scrollToLatest(true); + requestAnimationFrame(() => this.scrollToLatest(true)); + setTimeout(() => this.scrollToLatest(true), 50); } scrollToBottom() { - if (!this.scrollElement) return; - - // 优先将最新一行吸附到视口内,保留边距,避免“对齐页面底部”的跳动 - const snapped = this.scrollLatestIntoView(16); - if (snapped) { - this.needsScroll = false; - this.startIdleTimer(); - return; - } - - // 【修复黑屏】如果正在渲染,延迟滚动避免冲突 - if (this.isRendering) { - requestAnimationFrame(() => this.scrollToBottom()); - return; - } - - // 【优化】防抖机制,避免频繁滚动 - if (this.scrollTimer) { - clearTimeout(this.scrollTimer); - } - - // 使用微任务延迟,减少layout thrashing - this.scrollTimer = setTimeout(() => { - if (!this.scrollElement) return; - - // 【Phase 2.2优化】使用Layout缓存减少重排 - let scrollData; - if (this.layoutCache) { - scrollData = { - scrollHeight: this.layoutCache.get('scrollHeight'), - clientHeight: this.layoutCache.get('clientHeight'), - currentScroll: this.layoutCache.get('scrollTop') - }; - } else { - // 降级方案:批量读取layout属性 - scrollData = { - scrollHeight: this.scrollElement.scrollHeight, - clientHeight: this.scrollElement.clientHeight, - currentScroll: this.scrollElement.scrollTop - }; - } - - // 计算目标位置 - const targetScroll = scrollData.scrollHeight - scrollData.clientHeight; - const scrollDelta = Math.abs(scrollData.currentScroll - targetScroll); - - // 只在需要时滚动(距离>1px才滚动,避免微小抖动) - if (scrollDelta > 1) { - // 【优化吸附体验】根据滚动距离决定是否显示过渡 - // 短距离(<100px):平滑滚动 - // 长距离(>=100px):直接跳转,避免用户等待 - const useSmooth = scrollDelta < 100; - - // 【修复黑屏】锁定滚动,防止触发handleUserScroll - this.scrollLocked = true; - - // 使用requestAnimationFrame确保在合适的时机滚动 - requestAnimationFrame(() => { - if (this.scrollElement) { - if (useSmooth && this.scrollElement.scrollTo) { - // 平滑滚动(短距离) - this.scrollElement.scrollTo({ - top: targetScroll, - behavior: 'smooth' - }); - } else { - // 直接跳转(长距离或不支持scrollTo) - this.scrollElement.scrollTop = targetScroll; - } - - // 【Phase 2.2】滚动后使缓存失效 - if (this.layoutCache) { - this.layoutCache.invalidate(); - } - - // 【优化日志】只在大距离滚动时记录 - if (scrollDelta > 50) { - console.log(`[自动吸附] 滚动 ${Math.round(scrollDelta)}px 到底部 (${useSmooth ? '平滑' : '直接'})`); - } - } - this.needsScroll = false; - this.startIdleTimer(); - - // 解锁滚动(平滑滚动需要更长时间) - setTimeout(() => { - this.scrollLocked = false; - }, useSmooth ? 300 : 100); - }); - } else { - this.needsScroll = false; - } - - this.scrollTimer = null; - }, 16); // 约1帧的时间,减少频繁触发 - } - - setLineHeight(px) { - if (px > 0) this.lineHeight = px; - } - - /** - * 【优化大量日志】设置窗口激活状态,智能渲染策略 - * @param {boolean} active - 是否为活动窗口 - */ - setActive(active) { - const wasInactive = !this.isActive; - this.isActive = active; - - if (active) { - this.startWatchdog(); - - // 【新增逻辑】切换引擎时,重置为自动吸附状态 - // 默认用户鼠标未滑动3秒以上 - this.autoScrollEnabled = true; - this.needsScroll = true; - this.clearResumeTimer(); - - // 【修复】窗口激活时,清除渲染哈希,确保强制渲染 - if (wasInactive) { - this.lastRenderHash = null; - console.log('[窗口激活] 清除渲染哈希,启用自动吸附'); - } - - // 【优化大量日志】智能渲染策略 - const totalPending = this.pending.length; - const totalLines = this.lines.length; - - if (totalPending > 0) { - // 【关键优化】大量积压日志时,分优先级渲染 - if (totalPending > 500) { - console.log(`[大量日志] 检测到${totalPending}条积压日志,启用智能渲染`); - - // 步骤1:立即flush并渲染最新的可见部分(最多500行) - const visibleCount = Math.min(500, totalPending); - const visibleBatch = this.pending.splice(totalPending - visibleCount, visibleCount); - this.lines.push(...visibleBatch); - this.flushCount++; - this.lastFlushTime = Date.now(); - - // 立即渲染可见部分,用户马上能看到最新日志 - this.render(); - - // 【新增】立即滚动到最新日志(因为最新的已经渲染好了) - if (this.autoScrollEnabled) { - this.scrollToBottom(); - } - - // 步骤2:后台分批处理剩余历史日志(异步,不阻塞) - if (this.pending.length > 0) { - // 传递是否需要最后滚动的标志 - this.renderHistoryInBackground(this.autoScrollEnabled); - } - } else { - // 少量积压:分批处理(50行/批) - const batchSize = 50; - const renderBatch = () => { - if (this.pending.length > 0) { - const batch = this.pending.splice(0, Math.min(batchSize, this.pending.length)); - this.lines.push(...batch); - this.flushCount++; - this.lastFlushTime = Date.now(); - - if (this.pending.length > 0) { - requestAnimationFrame(renderBatch); - } else { - // 所有数据处理完,执行渲染 - this.needsRender = false; - if (this.lines.length > 1000) { - this.progressiveRender(); - } else { - this.scheduleRender(true); - } - - // 【新增】渲染完成后,如果启用自动滚动,吸附到最新 - if (this.autoScrollEnabled) { - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } - } - } - }; - requestAnimationFrame(renderBatch); - } - } else if (this.needsRender || wasInactive) { - // 没有pending但需要渲染 - this.needsRender = false; - - if (this.lines.length > 1000) { - this.progressiveRender(); - } else { - this.scheduleRender(true); - } - - // 【新增】自动滚动到底部(如果有日志) - if (this.autoScrollEnabled && this.lines.length > 0) { - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } - } else if (this.lines.length > 0) { - // 【新增】即使不需要渲染,也要确保滚动到最新位置 - if (this.autoScrollEnabled) { - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } - } - - // 保证切换后立即同步最新内容 - this.scheduleRender(true); - this.startIdleTimer(); - - if (this.lines.length > 0) { - requestAnimationFrame(() => this.scrollToBottom()); - } - } else { - this.stopWatchdog(); - this.clearIdleTimer(); - } - } - - /** - * 【新增】后台渐进式渲染历史日志,不阻塞UI - * @param {boolean} shouldScrollAfter - 渲染完成后是否自动滚动到最新 - */ - renderHistoryInBackground(shouldScrollAfter = false) { - const batchSize = 100; // 每批处理100行 - let processed = 0; - const totalToProcess = this.pending.length; - - // 显示状态栏提示 - console.log(`[后台渲染] 开始渲染${totalToProcess}条历史日志...`); - this.updateStatusBar('正在加载历史日志...', `共 ${totalToProcess} 条`, false); - - const processBatch = () => { - if (this.pending.length > 0 && processed < 5000) { - // 每次处理一批 - const batch = this.pending.splice(0, Math.min(batchSize, this.pending.length)); - this.lines.unshift(...batch); // 插入到开头(历史日志) - processed += batch.length; - - // 每处理500条,输出进度并更新状态栏 - if (processed % 500 === 0) { - const progress = Math.round((processed / totalToProcess) * 100); - console.log(`[后台渲染] 进度: ${progress}% (${processed}/${totalToProcess})`); - this.updateStatusBar( - '正在加载历史日志...', - `${progress}% (${processed}/${totalToProcess} 条)`, - false - ); - } - - // 使用idle callback,CPU空闲时才处理 - if (typeof requestIdleCallback !== 'undefined') { - requestIdleCallback(() => processBatch(), { timeout: 1000 }); - } else { - setTimeout(processBatch, 50); - } - } else { - // 全部处理完成,清空pending - this.pending = []; - console.log(`[后台渲染] 历史日志渲染完成,共处理${processed}条`); - - // 隐藏状态栏 - this.hideStatusBar(); - - // 触发一次完整渲染(可能需要trim) - this.lastRenderHash = null; - this.maybeTrim(); - - // 【新增】如果需要,渲染完成后自动滚动到最新 - if (shouldScrollAfter && this.autoScrollEnabled && this.lines.length > 0) { - console.log('[后台渲染] 完成后自动吸附到最新日志'); - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } - } - }; - - // 启动后台处理 - requestAnimationFrame(processBatch); + this.scrollToLatest(true); } append(text, className = 'console-line') { - const nearBottom = this.isNearBottom(); + if (!this.container || text === undefined || text === null) return; + const normalized = typeof text === 'string' ? text : String(text); + const stickToLatest = this.autoScrollEnabled || this.isNearBottom(); - // 在添加内容前检查是否在底部,如果是则标记需要滚动 - if (this.autoScrollEnabled && nearBottom) { + this.lines.push({ text: normalized, className: className || 'console-line' }); + const node = document.createElement('div'); + node.className = className || 'console-line'; + node.textContent = normalized; + this.container.appendChild(node); + + this.pendingHighWaterMark = Math.max(this.pendingHighWaterMark, this.lines.length); + this.lastRenderLineCount = this.lines.length; + this.lastRenderTime = performance.now(); + + if (stickToLatest) { this.needsScroll = true; - } else if (!this.autoScrollEnabled && nearBottom) { - // 用户并未真正离开底部,自动恢复吸附 - this.autoScrollEnabled = true; - this.needsScroll = true; - this.clearResumeTimer(); + // 推迟到下一帧,确保布局完成 + requestAnimationFrame(() => this.forceScrollToLatest()); } + } - this.pending.push({ text, className }); - this.pendingHighWaterMark = Math.max(this.pendingHighWaterMark, this.pending.length); - - // 队列堆积时给出进度提示(仅当前活动窗口) - if (this.isActive && this.pending.length > 150) { - this.updateStatusBar('日志渲染中...', `${this.pending.length} 条排队`, false, 1200); + appendBatch(items) { + if (!this.container || !Array.isArray(items) || items.length === 0) return; + const fragment = document.createDocumentFragment(); + let appended = 0; + items.forEach(item => { + if (item === undefined || item === null) return; + const text = typeof item === 'string' ? item : item.text; + if (text === undefined || text === null) return; + const className = typeof item === 'string' ? 'console-line' : (item.className || 'console-line'); + this.lines.push({ text, className }); + const node = document.createElement('div'); + node.className = className; + node.textContent = String(text); + fragment.appendChild(node); + appended += 1; + }); + if (appended > 0) { + this.container.appendChild(fragment); + this.pendingHighWaterMark = Math.max(this.pendingHighWaterMark, this.lines.length); + this.lastRenderLineCount = this.lines.length; + this.lastRenderTime = performance.now(); + if (this.autoScrollEnabled) { + this.scrollToLatest(true); + } } - - // 【修复黑屏】统一批处理逻辑 - 所有窗口都实时渲染 - // 清除之前的定时器 - if (this.flushTimer) { - clearTimeout(this.flushTimer); - this.flushTimer = null; - } - - // 达到阈值立即flush,否则延迟批处理 - if (this.pending.length >= this.batchThreshold) { - this.flush(); - this.scheduleRender(); - } else { - // 设置定时器批处理 - this.flushTimer = setTimeout(() => { - this.flushTimer = null; - this.flush(); - this.scheduleRender(); - }, this.batchDelay); - } - - // 如果容器当前是空的,强制渲染一次,避免“黑屏等待” - this.forceRenderIfBlank(); - - this.maybeTrim(); } clear(message = null) { + if (this.container) { + this.container.innerHTML = ''; + } this.lines = []; - this.pending = []; - // 不清空pool,复用DOM节点以提高性能 if (message) { - this.lines.push({ text: message, className: 'console-line' }); + this.append(message, 'console-line'); + } else { + this.needsScroll = true; } - this.lastRenderHash = null; - this.needsScroll = true; // 清空后需要滚动到底部 - // 【FIX Bug #3】清空时不使用异步渲染,由调用者决定何时渲染 - // 这样可以批量操作后再统一渲染,提高性能 } - flush() { - if (!this.pending.length) return; - - // 清理批处理定时器 - if (this.flushTimer) { - clearTimeout(this.flushTimer); - this.flushTimer = null; - } - - // 【优化】记录flush时间,用于自适应批处理 - this.lastFlushTime = Date.now(); - this.flushCount++; - - // 【优化】批量push,减少数组操作次数 - this.lines.push(...this.pending); - const flushedCount = this.pending.length; - this.pending = []; - - // 【优化】如果一次性flush的行数很多(>500),启用分帧渲染 - if (flushedCount > 500) { - console.log(`[性能优化] 检测到大批量日志(${flushedCount}行),启用分帧渲染`); - } - - this.maybeTrim(); - - // 【优化】返回flush的行数,供调用者决定渲染策略 - return flushedCount; - } - - maybeTrim() { - // 【优化】智能内存管理策略 - if (this.lines.length <= this.maxLines) return; - - const toDrop = this.lines.length - this.trimTarget; - if (toDrop > 0) { - // 【优化】批量删除,性能更好 - this.lines.splice(0, toDrop); - - // 重置哈希,强制下次渲染 - this.lastRenderHash = null; - - console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`); - - // 【优化】内存使用超过阈值时,强制垃圾回收提示 - const estimatedMemory = this.lines.length * 100 + this.pending.length * 100; - if (estimatedMemory > 5 * 1024 * 1024) { // 超过5MB - console.warn('[内存警告] 日志内存使用较高,建议刷新页面'); - } + render() { + if (!this.container) return; + const fragment = document.createDocumentFragment(); + this.lines.forEach(line => { + const node = document.createElement('div'); + node.className = line.className || 'console-line'; + node.textContent = line.text; + fragment.appendChild(node); + }); + this.container.replaceChildren(fragment); + this.lastRenderLineCount = this.lines.length; + this.lastRenderTime = performance.now(); + if (this.autoScrollEnabled || this.isNearBottom()) { + this.scrollToLatest(true); } } scheduleRender(force = false) { - if (!this.container) return; - - // 【修复黑屏】如果正在渲染,标记为pending,渲染完成后再次调用 - if (this.isRendering) { - this.renderPending = true; + if (force) { + this.render(); return; } - - if (!force && this.rafId) return; - - // 【修复切换黑屏】所有窗口都实时渲染 - // 确保pending数据被flush - if (this.pending.length > 0) { - this.flush(); - } - - // 取消之前的请求 - if (this.rafId) { - cancelAnimationFrame(this.rafId); - } - - // 【优化】使用requestAnimationFrame进行渲染调度 - this.rafId = requestAnimationFrame(() => { - this.rafId = null; - this.isRendering = true; // 设置渲染锁 - - try { - // 执行渲染(无论是否活动窗口) - const totalLines = this.lines.length; - if (totalLines > 1000) { - this.progressiveRender(); - } else { - this.render(); - } - } finally { - this.isRendering = false; // 释放渲染锁 - - // 如果渲染期间有新请求,立即再次渲染 - if (this.renderPending) { - this.renderPending = false; - this.scheduleRender(); - } - } + if (this.renderScheduled) return; + this.renderScheduled = true; + requestAnimationFrame(() => { + this.renderScheduled = false; + this.render(); }); } - render() { - this.flush(); - const total = this.lines.length; - - if (!total) { - if (this.container.innerHTML !== '') { - this.container.innerHTML = ''; - // 清空时也清理pool - this.pool = []; - this.beforeSpacer = null; - this.afterSpacer = null; - } - this.lastRenderAt = performance.now(); - return; - } - - // 【优化】改进内容哈希:包含活动状态,确保窗口切换时渲染 - const lastLine = this.lines[total - 1]; - const contentHash = `${total}-${lastLine ? lastLine.text : ''}-${this.isActive}`; - - // 检查是否需要强制渲染 - const forceRender = (this.needsScroll && this.autoScrollEnabled) || - !this.container.querySelector('.console-line'); // DOM为空时强制渲染 - - if (this.lastRenderHash === contentHash && !forceRender) { - // 内容没有变化且不需要强制渲染,跳过 - return; - } - this.lastRenderHash = contentHash; - this.lastRenderLineCount = total; - this.lastRenderAt = performance.now(); - - // 【优化】计算可见区域 - const lh = this.lineHeight; - const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1; - const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); - - let scrollTop = (this.scrollElement && this.scrollElement.scrollTop) || 0; - - // 【优化】初始渲染时,如果需要自动滚动,从底部开始显示 - // 这样用户能看到最新的日志而不是最旧的 - if (scrollTop === 0 && this.autoScrollEnabled && total > visible) { - // 模拟滚动到底部的scrollTop值 - scrollTop = Math.max(0, (total - visible) * lh); - // 标记需要实际滚动 - this.needsScroll = true; - } - - const halfVisible = Math.floor(visible / 2); - const rawStart = Math.floor(scrollTop / lh) - halfVisible; - - // 【Phase 3优化】添加预渲染缓冲区 - 上下各预渲染90行 - // 这使得滚动更流畅,无白屏 - const bufferStart = Math.max(0, rawStart - this.preRenderBuffer); - const bufferEnd = Math.min(total, rawStart + visible + this.preRenderBuffer); - - let start = bufferStart; - let end = bufferEnd; - let needed = Math.max(0, end - start); - - // 【修复白屏】先保证DOM池容量足够覆盖当前窗口 - this.ensurePoolCapacity(needed); - - // 如果窗口仍超过池容量,收缩窗口贴近最新日志,防止中间缺口 - if (needed > this.pool.length) { - const windowSize = this.pool.length || this.minPoolSize; - end = Math.min(total, Math.max(end, windowSize)); - start = Math.max(0, end - windowSize); - needed = Math.max(0, end - start); - } - - if (needed === 0 && total > 0) { - // 极端情况下兜底展示最新日志,避免空白 - end = total; - start = Math.max(0, end - Math.min(this.maxVisible, total)); - needed = end - start; - this.ensurePoolCapacity(needed); - } - - const beforeHeight = start * lh; - const afterHeight = (total - end) * lh; - - // 【优化】限制DOM节点池大小(在更新池容量后执行) - if (this.pool.length > this.maxPoolSize) { - const excess = this.pool.length - this.maxPoolSize; - this.pool.splice(this.maxPoolSize, excess).forEach(node => { - if (node && node.parentNode) { - node.parentNode.removeChild(node); - } - }); - } - - // 再次兜底,确保池容量与窗口一致 - this.ensurePoolCapacity(needed); - - // 【优化】复用或创建占位符 - if (!this.beforeSpacer) { - this.beforeSpacer = document.createElement('div'); - this.beforeSpacer.dataset.spacer = 'before'; - this.beforeSpacer.style.willChange = 'height'; // GPU加速 - } else if (!this.beforeSpacer.parentNode) { - this.beforeSpacer = document.createElement('div'); - this.beforeSpacer.dataset.spacer = 'before'; - this.beforeSpacer.style.willChange = 'height'; - } - this.beforeSpacer.style.height = `${beforeHeight}px`; - - if (!this.afterSpacer) { - this.afterSpacer = document.createElement('div'); - this.afterSpacer.dataset.spacer = 'after'; - this.afterSpacer.style.willChange = 'height'; // GPU加速 - } else if (!this.afterSpacer.parentNode) { - this.afterSpacer = document.createElement('div'); - this.afterSpacer.dataset.spacer = 'after'; - this.afterSpacer.style.willChange = 'height'; - } - this.afterSpacer.style.height = `${afterHeight}px`; - - // 【优化】使用DocumentFragment批量操作DOM - const fragment = document.createDocumentFragment(); - - // 【优化】只更新可见区域的节点 - for (let idx = start; idx < end; idx++) { - const line = this.lines[idx]; - const poolIdx = idx - start; - const node = this.pool[poolIdx]; - if (!node) continue; - - // 【优化】只在内容或类名变化时才更新节点 - if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) { - node.className = line.className || 'console-line'; - node.textContent = line.text; - } - fragment.appendChild(node); - } - - // 【优化】增量更新DOM,减少重绘 - const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode; - - if (needsRebuild) { - // 【优化】双缓冲渲染 - 避免黑屏空窗期 - // 先准备新内容,再一次性替换,保持界面始终有内容显示 - - // 如果容器有内容且是大量日志,显示状态栏加载提示 - if (this.container.childNodes.length > 0 && total > 500) { - this.updateStatusBar('正在渲染日志...', `共 ${total} 行`, true, 2000); - } - - // 使用requestAnimationFrame确保渲染流畅 - requestAnimationFrame(() => { - // 创建新的内容容器 - const newContent = document.createDocumentFragment(); - - // 添加beforeSpacer - newContent.appendChild(this.beforeSpacer); - - // 添加可见内容 - newContent.appendChild(fragment); - - // 添加afterSpacer - newContent.appendChild(this.afterSpacer); - - // 一次性替换所有子节点,避免闪烁 - // replaceChildren 是原子操作,比 innerHTML = '' 更高效 - this.container.replaceChildren(...newContent.childNodes); - - // 如果需要滚动到底部,延迟执行避免影响渲染 - if (this.needsScroll && this.autoScrollEnabled) { - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } - - this.lastRenderAt = performance.now(); - }); - } else { - // 【优化】增量更新:智能diff算法,只更新必要的节点 - const existingNodes = Array.from(this.container.querySelectorAll('.console-line')); - - // 计算需要的节点数量 - const needCount = end - start; - const existCount = existingNodes.length; - - if (existCount === needCount) { - // 节点数量相同,直接替换内容 - let i = 0; - for (let idx = start; idx < end; idx++) { - const line = this.lines[idx]; - const node = existingNodes[i]; - if (node) { - // 只在内容变化时更新 - if (node.textContent !== line.text) { - node.textContent = line.text; - } - if (node.className !== (line.className || 'console-line')) { - node.className = line.className || 'console-line'; - } - } - i++; - } - } else if (existCount > needCount) { - // 节点过多,移除多余的 - for (let i = needCount; i < existCount; i++) { - const node = existingNodes[i]; - if (node && node.parentNode === this.container) { - this.container.removeChild(node); - } - } - // 更新保留的节点内容 - let i = 0; - for (let idx = start; idx < end && i < needCount; idx++) { - const line = this.lines[idx]; - const node = existingNodes[i]; - if (node) { - if (node.textContent !== line.text) { - node.textContent = line.text; - } - if (node.className !== (line.className || 'console-line')) { - node.className = line.className || 'console-line'; - } - } - i++; - } - } else { - // 节点不足,复用现有的并添加新的 - // 先更新现有节点 - let i = 0; - for (; i < existCount; i++) { - const line = this.lines[start + i]; - const node = existingNodes[i]; - if (node && line) { - if (node.textContent !== line.text) { - node.textContent = line.text; - } - if (node.className !== (line.className || 'console-line')) { - node.className = line.className || 'console-line'; - } - } - } - // 添加不足的节点 - const newFragment = document.createDocumentFragment(); - for (let idx = start + existCount; idx < end; idx++) { - const line = this.lines[idx]; - const poolIdx = idx - start; - const node = this.pool[poolIdx]; - if (node) { - node.className = line.className || 'console-line'; - node.textContent = line.text; - newFragment.appendChild(node); - } - } - if (newFragment.childNodes.length > 0) { - this.container.insertBefore(newFragment, this.afterSpacer); - } - } - } - - // 【优化吸附逻辑】渲染完成后的智能滚动 - // 1. 如果自动滚动已启用且需要滚动 -> 立即滚动 - // 2. 如果自动滚动未启用但有恢复计时器 -> 提前触发吸附(用户已停止滚动一段时间) - // 3. 如果用户在底部附近 -> 保持在底部 - if (this.autoScrollEnabled && (this.needsScroll || this.isNearBottom())) { - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } else if (!this.autoScrollEnabled && this.resumeTimer) { - // 【新增】用户已停止滚动一段时间(计时器在运行),新日志渲染完成,提前吸附 - console.log('[自动吸附] 检测到新内容渲染完成,用户已停止滚动,提前吸附到最新'); + setActive(active) { + this.isActive = !!active; + if (active) { this.autoScrollEnabled = true; - this.clearResumeTimer(); - requestAnimationFrame(() => { - this.scrollToBottom(); - }); + // 立即多次吸附,避免切换时机导致停在中间 + this.forceScrollToLatest(); } - - this.lastRenderAt = performance.now(); } - /** - * 【新增】渐进式渲染方法 - 处理大量日志时分批渲染 - * 避免一次性渲染大量内容导致的卡顿和空窗期 - */ - progressiveRender() { - if (!this.container || this.isProgressiveRendering) return; + maybeTrim() { + // 保留所有日志,满足“完整展示”需求 + return; + } - this.isProgressiveRendering = true; - const total = this.lines.length; - - // 只对大量日志启用渐进式渲染 - if (total <= 500) { - this.render(); - this.isProgressiveRendering = false; - return; - } - - console.log(`[渐进式渲染] 开始渲染 ${total} 行日志`); - - // 显示初始状态 - this.updateStatusBar('正在加载日志...', `共 ${total} 行`, false); - - // 分批参数 - const batchSize = 200; // 每批渲染200行 - let currentBatch = 0; - const totalBatches = Math.ceil(total / batchSize); - this.lastRenderAt = performance.now(); - - // 显示渲染进度 - const showProgress = () => { - const progress = Math.round((currentBatch / totalBatches) * 100); - const processedLines = Math.min(currentBatch * batchSize, total); - this.updateStatusBar( - '正在渲染日志...', - `${progress}% (${processedLines}/${total} 行)`, - false - ); + getPerformanceStats() { + const memoryBytes = this.lines.length * 100; + const memoryEstimate = memoryBytes < 1024 + ? `${memoryBytes} B` + : `${(memoryBytes / 1024).toFixed(2)} KB`; + return { + totalLines: this.lines.length, + pendingLines: 0, + pendingHighWaterMark: Math.max(this.pendingHighWaterMark, this.lines.length), + flushCount: this.flushCount, + lastRenderTime: `${this.lastRenderTime ? this.lastRenderTime.toFixed(2) : 0}ms`, + lastRenderLineCount: this.lastRenderLineCount, + poolSize: this.lines.length, + memoryEstimate }; + } - // 渲染一批数据 - const renderBatch = () => { - const startIdx = currentBatch * batchSize; - const endIdx = Math.min((currentBatch + 1) * batchSize, total); + resetPerformanceStats() { + this.flushCount = 0; + this.pendingHighWaterMark = this.lines.length; + this.lastRenderTime = 0; + this.lastRenderLineCount = this.lines.length; + } - // 创建批量fragment - const batchFragment = document.createDocumentFragment(); - for (let i = startIdx; i < endIdx; i++) { - const line = this.lines[i]; - const node = document.createElement('div'); - node.className = line.className || 'console-line'; - node.textContent = line.text; - batchFragment.appendChild(node); - } - - // 如果是第一批,清理旧内容 - if (currentBatch === 0) { - this.container.replaceChildren(); - } - - // 追加新批次 - this.container.appendChild(batchFragment); - - currentBatch++; - showProgress(); - - // 继续下一批或完成 - if (currentBatch < totalBatches) { - requestAnimationFrame(renderBatch); - } else { - console.log(`[渐进式渲染] 完成,共渲染 ${total} 行`); - this.isProgressiveRendering = false; - - // 隐藏状态栏 - this.hideStatusBar(); - this.lastRenderAt = performance.now(); - - // 【优化吸附】渲染完成后的智能滚动 - // 1. 自动滚动已启用 -> 直接吸附 - // 2. 自动滚动未启用但有恢复计时器 -> 提前吸附(用户已停止滚动) - if (this.autoScrollEnabled && (this.needsScroll || this.isNearBottom())) { - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } else if (!this.autoScrollEnabled && this.resumeTimer) { - console.log('[自动吸附] 渐进式渲染完成,用户已停止滚动,提前吸附到最新'); - this.autoScrollEnabled = true; - this.clearResumeTimer(); - requestAnimationFrame(() => { - this.scrollToBottom(); - }); - } - } - }; - - // 开始渲染第一批 - showProgress(); - requestAnimationFrame(renderBatch); + setLineHeight() { + // 兼容旧接口,现为无操作 } } @@ -2993,6 +1847,8 @@ initializeEventListeners(); ensureSystemReadyOnLoad(); loadConsoleOutput(currentApp); + // 后台预加载其他引擎的历史日志,避免切换时空白 + setTimeout(preloadAllConsoleOutputs, 400); // 使用新的定时器管理系统 updateTime(); // 立即更新一次 @@ -3058,7 +1914,7 @@ socket.on('console_output', function(data) { // 处理控制台输出 addConsoleOutput(data.line, data.app); - + // 如果是forum的输出,同时也处理为论坛消息 if (data.app === 'forum') { const parsed = parseForumMessage(data.line); @@ -3254,9 +2110,9 @@ const fieldsHtml = group.fields.map(field => { const value = values[field.key] !== undefined ? values[field.key] : ''; const safeValue = escapeHtml(String(value || '')); - + let control; - + if (field.type === 'select' && field.options) { // 下拉选择框 const optionsHtml = field.options.map(option => { @@ -3355,37 +2211,37 @@ `; - + // 使用事件委托,只在容器上绑定一次事件 const container = document.getElementById('configFormContainer'); if (!container) { return; } - + // 防止重复绑定 if (container.dataset.passwordToggleAttached === 'true') { return; } - + container.addEventListener('click', (event) => { // 查找是否点击了密码切换按钮或其内部的SVG const toggle = event.target.closest('.config-password-toggle'); if (!toggle) { return; } - + const key = toggle.dataset.target; const input = container.querySelector(`.config-field-input[data-config-key="${key}"]`); if (!input) { return; } - + const reveal = input.getAttribute('type') === 'password'; input.setAttribute('type', reveal ? 'text' : 'password'); toggle.innerHTML = reveal ? eyeOnIcon : eyeOffIcon; toggle.classList.toggle('revealed', reveal); }); - + // 标记已绑定,防止重复 container.dataset.passwordToggleAttached = 'true'; } @@ -3740,6 +2596,27 @@ // 更新嵌入页面(右侧内容区域) updateEmbeddedPage(app); + // Forum默认吸附到最新日志与聊天 + if (app === 'forum') { + scrollForumViewToBottom(); + // 再次异步确认,避免布局切换时机导致未到底 + setTimeout(scrollForumViewToBottom, 200); + } + + // Report默认吸附到最新日志 + if (app === 'report') { + scrollReportViewToBottom(); + setTimeout(scrollReportViewToBottom, 200); + } + + // 其他引擎也补充双重吸附,降低偶发不贴底 + setTimeout(() => { + const renderer = logRenderers[app]; + if (renderer) { + renderer.forceScrollToLatest(); + } + }, 120); + // 【图层优化】移除重复加载逻辑 // 日志数据已通过Socket.IO/SSE实时同步,无需重新加载 // 仅保留特殊页面的初始化逻辑 @@ -3826,11 +2703,14 @@ }, 5 * 60 * 1000); // 5分钟 } - // 存储最后显示的行数,避免重复加载 - let lastLineCount = {}; + +// 存储最后显示的行数,避免重复加载 + let lastLineCount = {}; - function getConsoleContainer() { + + +function getConsoleContainer() { return document.getElementById('consoleOutput'); } @@ -3932,13 +2812,7 @@ renderer.setActive(true); // 会在内部异步渲染待处理内容 renderer.needsScroll = true; renderer.scheduleRender(true); - - // 确保滚动到底部 - if (renderer.autoScrollEnabled) { - requestAnimationFrame(() => { - renderer.scrollToBottom(); - }); - } + requestAnimationFrame(() => renderer.forceScrollToLatest()); } } @@ -3989,23 +2863,24 @@ fetch(`/api/output/${app}`) .then(response => response.json()) .then(data => { - // 【FIX Bug #5】检查是否仍然是当前app,避免竞态条件 - // 如果用户已经切换到其他app,忽略这个响应 - if (currentApp !== app) { - console.log(`忽略${app}的日志响应(当前app是${currentApp})`); - return; - } - if (data.success && data.output.length > 0) { const lastCount = lastLineCount[app] || 0; const newLines = data.output.slice(lastCount); if (newLines.length > 0) { - newLines.forEach(line => { - appendConsoleTextLine(app, line); - }); + newLines.forEach(line => appendConsoleTextLine(app, line)); lastLineCount[app] = data.output.length; + // 切换到该引擎时立即吸附到最新,显示最新日志 + if (currentApp === app) { + const renderer = logRenderers[app]; + if (renderer) { + renderer.needsScroll = true; + requestAnimationFrame(() => renderer.forceScrollToLatest()); + setTimeout(() => renderer.forceScrollToLatest(), 60); + } + } + // 数据加载完成,更新加载提示为实际日志 const renderer = logRenderers[app]; if (renderer && renderer.lines.length > 0) { @@ -4033,6 +2908,14 @@ }); } + // 预加载所有Engine的历史日志,切换时无需等待 + function preloadAllConsoleOutputs() { + ['insight', 'media', 'query', 'forum'].forEach(app => { + if (app === currentApp) return; + loadConsoleOutput(app); + }); + } + // 刷新当前应用的控制台输出 function refreshConsoleOutput() { if (currentApp === 'forum') { @@ -5141,6 +4024,26 @@ chatArea.scrollTop = chatArea.scrollHeight; } + function scrollForumViewToBottom() { + const renderer = logRenderers['forum']; + if (renderer) { + requestAnimationFrame(() => renderer.scrollToBottom()); + } + const chatArea = document.getElementById('forumChatArea'); + if (chatArea) { + requestAnimationFrame(() => { + chatArea.scrollTop = chatArea.scrollHeight; + }); + } + } + + function scrollReportViewToBottom() { + const renderer = logRenderers['report']; + if (renderer) { + requestAnimationFrame(() => renderer.scrollToBottom()); + } + } + // 格式化消息内容 function formatMessageContent(content) { if (!content) return '';