Optimize Log Output Efficiency
This commit is contained in:
@@ -372,49 +372,50 @@ def parse_forum_log_line(line):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Forum日志监听器
|
# Forum日志监听器
|
||||||
|
# 存储每个客户端的历史日志发送位置
|
||||||
|
forum_log_positions = {}
|
||||||
|
|
||||||
def monitor_forum_log():
|
def monitor_forum_log():
|
||||||
"""监听forum.log文件变化并推送到前端"""
|
"""监听forum.log文件变化并推送到前端"""
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
forum_log_file = LOG_DIR / "forum.log"
|
forum_log_file = LOG_DIR / "forum.log"
|
||||||
last_position = 0
|
last_position = 0
|
||||||
processed_lines = set() # 用于跟踪已处理的行,避免重复
|
processed_lines = set() # 用于跟踪已处理的行,避免重复
|
||||||
|
|
||||||
# 如果文件存在,获取初始位置
|
# 如果文件存在,获取初始位置但不跳过内容
|
||||||
if forum_log_file.exists():
|
if forum_log_file.exists():
|
||||||
with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
# 初始化时读取所有现有行,避免重复处理
|
# 记录文件大小,但不添加到processed_lines
|
||||||
existing_lines = f.readlines()
|
# 这样用户打开forum标签时可以获取历史
|
||||||
for line in existing_lines:
|
f.seek(0, 2) # 移到文件末尾
|
||||||
line_hash = hash(line.strip())
|
|
||||||
processed_lines.add(line_hash)
|
|
||||||
last_position = f.tell()
|
last_position = f.tell()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if forum_log_file.exists():
|
if forum_log_file.exists():
|
||||||
with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
f.seek(last_position)
|
f.seek(last_position)
|
||||||
new_lines = f.readlines()
|
new_lines = f.readlines()
|
||||||
|
|
||||||
if new_lines:
|
if new_lines:
|
||||||
for line in new_lines:
|
for line in new_lines:
|
||||||
line = line.rstrip('\n\r')
|
line = line.rstrip('\n\r')
|
||||||
if line.strip():
|
if line.strip():
|
||||||
line_hash = hash(line.strip())
|
line_hash = hash(line.strip())
|
||||||
|
|
||||||
# 避免重复处理同一行
|
# 避免重复处理同一行
|
||||||
if line_hash in processed_lines:
|
if line_hash in processed_lines:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
processed_lines.add(line_hash)
|
processed_lines.add(line_hash)
|
||||||
|
|
||||||
# 解析日志行并发送forum消息
|
# 解析日志行并发送forum消息
|
||||||
parsed_message = parse_forum_log_line(line)
|
parsed_message = parse_forum_log_line(line)
|
||||||
if parsed_message:
|
if parsed_message:
|
||||||
socketio.emit('forum_message', parsed_message)
|
socketio.emit('forum_message', parsed_message)
|
||||||
|
|
||||||
# 只有在控制台显示forum时才发送控制台消息
|
# 只有在控制台显示forum时才发送控制台消息
|
||||||
timestamp = datetime.now().strftime('%H:%M:%S')
|
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||||
formatted_line = f"[{timestamp}] {line}"
|
formatted_line = f"[{timestamp}] {line}"
|
||||||
@@ -422,13 +423,15 @@ def monitor_forum_log():
|
|||||||
'app': 'forum',
|
'app': 'forum',
|
||||||
'line': formatted_line
|
'line': formatted_line
|
||||||
})
|
})
|
||||||
|
|
||||||
last_position = f.tell()
|
last_position = f.tell()
|
||||||
|
|
||||||
# 清理processed_lines集合,避免内存泄漏(保留最近1000行的哈希)
|
# 清理processed_lines集合,避免内存泄漏(保留最近1000行的哈希)
|
||||||
if len(processed_lines) > 1000:
|
if len(processed_lines) > 1000:
|
||||||
processed_lines.clear()
|
# 保留最近500行的哈希
|
||||||
|
recent_hashes = list(processed_lines)[-500:]
|
||||||
|
processed_lines = set(recent_hashes)
|
||||||
|
|
||||||
time.sleep(1) # 每秒检查一次
|
time.sleep(1) # 每秒检查一次
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Forum日志监听错误: {e}")
|
logger.error(f"Forum日志监听错误: {e}")
|
||||||
@@ -903,6 +906,57 @@ def get_forum_log():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'message': f'读取forum.log失败: {str(e)}'})
|
return jsonify({'success': False, 'message': f'读取forum.log失败: {str(e)}'})
|
||||||
|
|
||||||
|
@app.route('/api/forum/log/history', methods=['POST'])
|
||||||
|
def get_forum_log_history():
|
||||||
|
"""获取Forum历史日志(支持从指定位置开始)"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
start_position = data.get('position', 0) # 客户端上次接收的位置
|
||||||
|
max_lines = data.get('max_lines', 1000) # 最多返回的行数
|
||||||
|
|
||||||
|
forum_log_file = LOG_DIR / "forum.log"
|
||||||
|
if not forum_log_file.exists():
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'log_lines': [],
|
||||||
|
'position': 0,
|
||||||
|
'has_more': False
|
||||||
|
})
|
||||||
|
|
||||||
|
with open(forum_log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
# 从指定位置开始读取
|
||||||
|
f.seek(start_position)
|
||||||
|
lines = []
|
||||||
|
line_count = 0
|
||||||
|
|
||||||
|
for line in f:
|
||||||
|
if line_count >= max_lines:
|
||||||
|
break
|
||||||
|
line = line.rstrip('\n\r')
|
||||||
|
if line.strip():
|
||||||
|
# 添加时间戳
|
||||||
|
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||||
|
formatted_line = f"[{timestamp}] {line}"
|
||||||
|
lines.append(formatted_line)
|
||||||
|
line_count += 1
|
||||||
|
|
||||||
|
# 记录当前位置
|
||||||
|
current_position = f.tell()
|
||||||
|
|
||||||
|
# 检查是否还有更多内容
|
||||||
|
f.seek(0, 2) # 移到文件末尾
|
||||||
|
end_position = f.tell()
|
||||||
|
has_more = current_position < end_position
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'log_lines': lines,
|
||||||
|
'position': current_position,
|
||||||
|
'has_more': has_more
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'message': f'读取forum历史失败: {str(e)}'})
|
||||||
|
|
||||||
@app.route('/api/search', methods=['POST'])
|
@app.route('/api/search', methods=['POST'])
|
||||||
def search():
|
def search():
|
||||||
"""统一搜索接口"""
|
"""统一搜索接口"""
|
||||||
|
|||||||
+455
-103
@@ -329,7 +329,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.console-layer {
|
.console-layer {
|
||||||
visibility: hidden; /* 使用visibility替代display,避免重排 */
|
/* 【优化】使用transform代替visibility,GPU加速避免重绘 */
|
||||||
position: absolute; /* 相对于.console-output绝对定位 */
|
position: absolute; /* 相对于.console-output绝对定位 */
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -338,20 +338,56 @@
|
|||||||
padding: 15px; /* 图层内边距 */
|
padding: 15px; /* 图层内边距 */
|
||||||
overflow-y: auto; /* 允许独立滚动 */
|
overflow-y: auto; /* 允许独立滚动 */
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
pointer-events: none; /* 隐藏层不响应交互 */
|
|
||||||
box-sizing: border-box; /* 包含padding在width/height内 */
|
box-sizing: border-box; /* 包含padding在width/height内 */
|
||||||
|
/* GPU加速优化 */
|
||||||
|
transform: translateX(100%); /* 默认移出视图 */
|
||||||
|
will-change: transform; /* 提示浏览器优化transform */
|
||||||
|
backface-visibility: hidden; /* 避免闪烁 */
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
opacity: 0; /* 配合transform使用 */
|
||||||
|
pointer-events: none; /* 隐藏层不响应交互 */
|
||||||
|
/* 平滑切换 */
|
||||||
|
transition: transform 0.15s ease-out, opacity 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-layer.active {
|
.console-layer.active {
|
||||||
visibility: visible; /* 显示活动层 */
|
/* 【优化】活动层使用transform归位,高性能切换 */
|
||||||
pointer-events: auto; /* 活动层响应交互 */
|
transform: translateX(0); /* 移回视图 */
|
||||||
z-index: 1; /* 置顶显示 */
|
opacity: 1; /* 完全可见 */
|
||||||
|
pointer-events: auto; /* 活动层响应交互 */
|
||||||
|
z-index: 1; /* 置顶显示 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line {
|
.console-line {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 【新增】加载状态指示器样式 */
|
||||||
|
.console-line.loading-indicator {
|
||||||
|
color: #00ff00;
|
||||||
|
font-style: italic;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-line.render-progress {
|
||||||
|
color: #00ff00;
|
||||||
|
background: linear-gradient(90deg, rgba(0,255,0,0.1) 0%, transparent 100%);
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-left: 3px solid #00ff00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 0.6; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 渐进式渲染时的占位符 */
|
||||||
|
.console-line.placeholder {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* 状态信息 */
|
/* 状态信息 */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
@@ -1379,8 +1415,8 @@
|
|||||||
this.pool = [];
|
this.pool = [];
|
||||||
this.lineHeight = 18;
|
this.lineHeight = 18;
|
||||||
this.maxVisible = 120;
|
this.maxVisible = 120;
|
||||||
this.maxLines = 1000; // 【优化】增加到1000行,提高缓存能力
|
this.maxLines = 10000; // 【优化】保留10000行历史,平衡内存和使用体验
|
||||||
this.trimTarget = 600; // 【优化】裁剪后保留600行
|
this.trimTarget = 8000; // 【优化】裁剪后保留8000行,避免频繁触发trim
|
||||||
this.maxPoolSize = 200; // 限制DOM节点池大小
|
this.maxPoolSize = 200; // 限制DOM节点池大小
|
||||||
this.rafId = null;
|
this.rafId = null;
|
||||||
this.autoScrollEnabled = true;
|
this.autoScrollEnabled = true;
|
||||||
@@ -1394,9 +1430,9 @@
|
|||||||
this.beforeSpacer = null;
|
this.beforeSpacer = null;
|
||||||
this.afterSpacer = null;
|
this.afterSpacer = null;
|
||||||
|
|
||||||
// 【新增】批处理优化参数
|
// 【优化】批处理参数 - 降低延迟提升响应速度
|
||||||
this.batchThreshold = 200; // 累积200行才flush(原50行)
|
this.batchThreshold = 50; // 累积50行就flush,减少延迟
|
||||||
this.batchDelay = 500; // 延迟500ms才flush(原200ms)
|
this.batchDelay = 100; // 延迟100ms就flush,大幅降低延迟
|
||||||
this.lastFlushTime = 0;
|
this.lastFlushTime = 0;
|
||||||
this.flushCount = 0;
|
this.flushCount = 0;
|
||||||
|
|
||||||
@@ -1560,39 +1596,40 @@
|
|||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
if (!this.scrollElement) return;
|
if (!this.scrollElement) return;
|
||||||
|
|
||||||
// 【FIX Bug #6】使用try-catch防止死锁
|
// 【优化】防抖机制,避免频繁滚动
|
||||||
try {
|
if (this.scrollTimer) {
|
||||||
// 使用锁防止重入
|
clearTimeout(this.scrollTimer);
|
||||||
if (this.scrollLocked) return;
|
|
||||||
this.scrollLocked = true;
|
|
||||||
|
|
||||||
// 【FIX Bug #2】不使用节流,确保每次调用都能滚动
|
|
||||||
// 移除节流逻辑,因为scheduleRender已经有节流了
|
|
||||||
|
|
||||||
// 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
try {
|
|
||||||
if (!this.scrollElement) {
|
|
||||||
this.scrollLocked = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接滚动到底部,不使用平滑滚动以避免性能问题
|
|
||||||
const targetScroll = this.scrollElement.scrollHeight;
|
|
||||||
this.scrollElement.scrollTop = targetScroll;
|
|
||||||
|
|
||||||
// 【FIX Bug #2】立即重置标志,不延迟
|
|
||||||
this.scrollLocked = false;
|
|
||||||
this.needsScroll = false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('滚动到底部失败:', e);
|
|
||||||
this.scrollLocked = false; // 确保锁被释放
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('scrollToBottom失败:', e);
|
|
||||||
this.scrollLocked = false; // 确保锁被释放
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用微任务延迟,减少layout thrashing
|
||||||
|
this.scrollTimer = setTimeout(() => {
|
||||||
|
if (!this.scrollElement) return;
|
||||||
|
|
||||||
|
// 【优化】批量读取layout属性,减少重排
|
||||||
|
const scrollData = {
|
||||||
|
scrollHeight: this.scrollElement.scrollHeight,
|
||||||
|
clientHeight: this.scrollElement.clientHeight,
|
||||||
|
currentScroll: this.scrollElement.scrollTop
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算目标位置
|
||||||
|
const targetScroll = scrollData.scrollHeight - scrollData.clientHeight;
|
||||||
|
|
||||||
|
// 只在需要时滚动
|
||||||
|
if (Math.abs(scrollData.currentScroll - targetScroll) > 1) {
|
||||||
|
// 使用requestAnimationFrame确保在合适的时机滚动
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this.scrollElement) {
|
||||||
|
this.scrollElement.scrollTop = targetScroll;
|
||||||
|
}
|
||||||
|
this.needsScroll = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.needsScroll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollTimer = null;
|
||||||
|
}, 16); // 约1帧的时间,减少频繁触发
|
||||||
}
|
}
|
||||||
|
|
||||||
setLineHeight(px) {
|
setLineHeight(px) {
|
||||||
@@ -1600,20 +1637,67 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 【图层优化】设置窗口激活状态
|
* 【优化】设置窗口激活状态,分批异步渲染积压内容
|
||||||
* @param {boolean} active - 是否为活动窗口
|
* @param {boolean} active - 是否为活动窗口
|
||||||
*/
|
*/
|
||||||
setActive(active) {
|
setActive(active) {
|
||||||
|
const wasInactive = !this.isActive;
|
||||||
this.isActive = active;
|
this.isActive = active;
|
||||||
if (active && this.needsRender) {
|
|
||||||
// 窗口激活时,如果有待渲染内容,异步渲染
|
if (active) {
|
||||||
requestIdleCallback(() => {
|
// 【修复】窗口激活时,清除渲染哈希,确保强制渲染
|
||||||
if (this.pending.length > 0) {
|
// 这解决了需要滚动才能显示内容的Bug
|
||||||
this.flush();
|
if (wasInactive) {
|
||||||
}
|
this.lastRenderHash = null; // 清除哈希,强制重新渲染
|
||||||
this.scheduleRender(true);
|
console.log('[窗口激活] 清除渲染哈希,准备强制渲染');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理积压的pending数据
|
||||||
|
if (this.pending.length > 0) {
|
||||||
|
// 窗口激活时,分批处理积压内容
|
||||||
|
const batchSize = 100; // 每批处理100行
|
||||||
|
const renderBatch = () => {
|
||||||
|
if (this.pending.length > 0) {
|
||||||
|
// 取出一批数据进行flush
|
||||||
|
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); // 强制渲染
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
requestAnimationFrame(renderBatch);
|
||||||
|
} else if (this.needsRender || wasInactive) {
|
||||||
|
// 【关键修复】即使needsRender为false,从非活动切换到活动也要渲染
|
||||||
this.needsRender = false;
|
this.needsRender = false;
|
||||||
}, { timeout: 50 });
|
|
||||||
|
// 强制渲染一次,确保内容显示
|
||||||
|
if (this.lines.length > 1000) {
|
||||||
|
this.progressiveRender(); // 大量数据用渐进式渲染
|
||||||
|
} else {
|
||||||
|
this.scheduleRender(true); // 强制渲染
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要自动滚动到底部
|
||||||
|
if (this.autoScrollEnabled && this.lines.length > 0) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1630,15 +1714,23 @@
|
|||||||
this.pendingHighWaterMark = this.pending.length;
|
this.pendingHighWaterMark = this.pending.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【图层优化】非活动窗口处理策略
|
// 【优化】非活动窗口延迟渲染策略
|
||||||
if (!this.isActive) {
|
if (!this.isActive) {
|
||||||
// 非活动窗口:只累积数据,不触发渲染
|
// 非活动窗口:延迟渲染,但不完全停止
|
||||||
// 设置队列上限,防止内存溢出
|
// 每500ms或累积100行就flush一次,保持数据流动
|
||||||
if (this.pending.length >= 1000) {
|
if (this.pending.length >= 100 ||
|
||||||
this.flush(); // 定期flush避免内存溢出
|
(this.pending.length > 0 && Date.now() - this.lastFlushTime > 500)) {
|
||||||
|
this.flush(); // 定期flush,避免积压太多
|
||||||
this.needsRender = true; // 标记需要渲染
|
this.needsRender = true; // 标记需要渲染
|
||||||
}
|
}
|
||||||
return; // 跳过后续渲染逻辑
|
// 不立即渲染,但设置延迟渲染
|
||||||
|
if (this.pending.length === 1 && !this.flushTimer) {
|
||||||
|
this.flushTimer = setTimeout(() => {
|
||||||
|
this.flush();
|
||||||
|
this.needsRender = true;
|
||||||
|
}, 500); // 非活动窗口500ms延迟
|
||||||
|
}
|
||||||
|
return; // 跳过立即渲染
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【优化】活动窗口:智能批处理策略
|
// 【优化】活动窗口:智能批处理策略
|
||||||
@@ -1656,10 +1748,10 @@
|
|||||||
clearTimeout(this.flushTimer);
|
clearTimeout(this.flushTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【优化】自适应延迟:如果最近flush频繁,说明日志流量大,缩短延迟
|
// 【优化】自适应延迟:根据流量动态调整延迟,提升响应速度
|
||||||
const adaptiveDelay = (timeSinceLastFlush < 1000)
|
const adaptiveDelay = (timeSinceLastFlush < 1000)
|
||||||
? Math.max(100, this.batchDelay / 2) // 高流量:缩短延迟
|
? Math.max(20, this.batchDelay / 4) // 高流量:大幅缩短延迟至20-25ms
|
||||||
: this.batchDelay; // 正常流量:使用标准延迟
|
: this.batchDelay; // 正常流量:使用标准延迟100ms
|
||||||
|
|
||||||
this.flushTimer = setTimeout(() => {
|
this.flushTimer = setTimeout(() => {
|
||||||
this.flush();
|
this.flush();
|
||||||
@@ -1715,7 +1807,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
maybeTrim() {
|
maybeTrim() {
|
||||||
// 【优化】更积极的trim策略,但保留更多行
|
// 【优化】智能内存管理策略
|
||||||
if (this.lines.length <= this.maxLines) return;
|
if (this.lines.length <= this.maxLines) return;
|
||||||
|
|
||||||
const toDrop = this.lines.length - this.trimTarget;
|
const toDrop = this.lines.length - this.trimTarget;
|
||||||
@@ -1727,6 +1819,12 @@
|
|||||||
this.lastRenderHash = null;
|
this.lastRenderHash = null;
|
||||||
|
|
||||||
console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`);
|
console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`);
|
||||||
|
|
||||||
|
// 【优化】内存使用超过阈值时,强制垃圾回收提示
|
||||||
|
const estimatedMemory = this.lines.length * 100 + this.pending.length * 100;
|
||||||
|
if (estimatedMemory > 5 * 1024 * 1024) { // 超过5MB
|
||||||
|
console.warn('[内存警告] 日志内存使用较高,建议刷新页面');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1756,13 +1854,21 @@
|
|||||||
// 【优化】性能监控:记录渲染开始时间
|
// 【优化】性能监控:记录渲染开始时间
|
||||||
const renderStart = performance.now();
|
const renderStart = performance.now();
|
||||||
|
|
||||||
this.render();
|
// 【优化】根据数据量选择渲染策略
|
||||||
|
const totalLines = this.lines.length + this.pending.length;
|
||||||
|
if (totalLines > 1000) {
|
||||||
|
// 大量数据使用渐进式渲染
|
||||||
|
this.progressiveRender();
|
||||||
|
} else {
|
||||||
|
// 少量数据直接渲染
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
// 【优化】记录渲染耗时
|
// 【优化】记录渲染耗时
|
||||||
this.renderTime = performance.now() - renderStart;
|
this.renderTime = performance.now() - renderStart;
|
||||||
|
|
||||||
// 【性能警告】如果渲染耗时超过16ms(一帧),输出警告
|
// 【性能警告】如果渲染耗时超过16ms(一帧),输出警告
|
||||||
if (this.renderTime > 16) {
|
if (this.renderTime > 16 && totalLines < 1000) {
|
||||||
console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`);
|
console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1783,15 +1889,16 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【优化】改进内容哈希:使用总行数+最后一行文本
|
// 【优化】改进内容哈希:包含活动状态,确保窗口切换时渲染
|
||||||
const lastLine = this.lines[total - 1];
|
const lastLine = this.lines[total - 1];
|
||||||
const contentHash = `${total}-${lastLine ? lastLine.text : ''}`;
|
const contentHash = `${total}-${lastLine ? lastLine.text : ''}-${this.isActive}`;
|
||||||
|
|
||||||
// 如果需要滚动,强制渲染
|
// 检查是否需要强制渲染
|
||||||
const forceRender = this.needsScroll && this.autoScrollEnabled;
|
const forceRender = (this.needsScroll && this.autoScrollEnabled) ||
|
||||||
|
!this.container.querySelector('.console-line'); // DOM为空时强制渲染
|
||||||
|
|
||||||
if (this.lastRenderHash === contentHash && !forceRender) {
|
if (this.lastRenderHash === contentHash && !forceRender) {
|
||||||
// 内容没有变化且不需要滚动,跳过渲染
|
// 内容没有变化且不需要强制渲染,跳过
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.lastRenderHash = contentHash;
|
this.lastRenderHash = contentHash;
|
||||||
@@ -1802,7 +1909,17 @@
|
|||||||
const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1;
|
const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1;
|
||||||
const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible);
|
const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible);
|
||||||
|
|
||||||
const scrollTop = (this.scrollElement && this.scrollElement.scrollTop) || 0;
|
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 halfVisible = Math.floor(visible / 2);
|
||||||
const rawStart = Math.floor(scrollTop / lh) - halfVisible;
|
const rawStart = Math.floor(scrollTop / lh) - halfVisible;
|
||||||
const start = Math.max(0, Math.min(total, rawStart));
|
const start = Math.max(0, Math.min(total, rawStart));
|
||||||
@@ -1878,27 +1995,128 @@
|
|||||||
const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode;
|
const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode;
|
||||||
|
|
||||||
if (needsRebuild) {
|
if (needsRebuild) {
|
||||||
// 需要完全重建
|
// 【优化】双缓冲渲染 - 避免黑屏空窗期
|
||||||
this.container.innerHTML = '';
|
// 先准备新内容,再一次性替换,保持界面始终有内容显示
|
||||||
this.container.appendChild(this.beforeSpacer);
|
|
||||||
this.container.appendChild(fragment);
|
|
||||||
this.container.appendChild(this.afterSpacer);
|
|
||||||
} else {
|
|
||||||
// 【优化】只更新可见节点部分,使用更高效的方式
|
|
||||||
const existingNodes = Array.from(this.container.querySelectorAll('.console-line'));
|
|
||||||
|
|
||||||
// 【优化】批量移除,减少重排
|
// 如果容器有内容且是大量日志,显示加载提示
|
||||||
if (existingNodes.length > 0) {
|
if (this.container.childNodes.length > 0 && total > 500) {
|
||||||
// 使用DocumentFragment收集要移除的节点
|
// 创建加载提示
|
||||||
existingNodes.forEach(node => {
|
const loadingDiv = document.createElement('div');
|
||||||
if (node.parentNode === this.container) {
|
loadingDiv.className = 'console-line loading-indicator';
|
||||||
this.container.removeChild(node);
|
loadingDiv.textContent = `[系统] 正在渲染 ${total} 行日志,请稍候...`;
|
||||||
}
|
loadingDiv.style.opacity = '0.7';
|
||||||
});
|
|
||||||
|
// 只在容器为空或没有加载提示时添加
|
||||||
|
if (!this.container.querySelector('.loading-indicator')) {
|
||||||
|
this.container.insertBefore(loadingDiv, this.container.firstChild);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在占位符之间插入新节点
|
// 使用requestAnimationFrame确保加载提示显示
|
||||||
this.container.insertBefore(fragment, this.afterSpacer);
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【优化】如果需要滚动且自动滚动启用,立即滚动
|
// 【优化】如果需要滚动且自动滚动启用,立即滚动
|
||||||
@@ -1906,6 +2124,104 @@
|
|||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 【新增】渐进式渲染方法 - 处理大量日志时分批渲染
|
||||||
|
* 避免一次性渲染大量内容导致的卡顿和空窗期
|
||||||
|
*/
|
||||||
|
progressiveRender() {
|
||||||
|
if (!this.container || this.isProgressiveRendering) return;
|
||||||
|
|
||||||
|
this.isProgressiveRendering = true;
|
||||||
|
const total = this.lines.length;
|
||||||
|
|
||||||
|
// 只对大量日志启用渐进式渲染
|
||||||
|
if (total <= 500) {
|
||||||
|
this.render();
|
||||||
|
this.isProgressiveRendering = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[渐进式渲染] 开始渲染 ${total} 行日志`);
|
||||||
|
|
||||||
|
// 分批参数
|
||||||
|
const batchSize = 200; // 每批渲染200行
|
||||||
|
let currentBatch = 0;
|
||||||
|
const totalBatches = Math.ceil(total / batchSize);
|
||||||
|
|
||||||
|
// 显示渲染进度
|
||||||
|
const showProgress = () => {
|
||||||
|
const progress = Math.round((currentBatch / totalBatches) * 100);
|
||||||
|
const progressDiv = this.container.querySelector('.render-progress');
|
||||||
|
if (progressDiv) {
|
||||||
|
progressDiv.textContent = `[系统] 渲染进度: ${progress}% (${Math.min(currentBatch * batchSize, total)}/${total} 行)`;
|
||||||
|
} else {
|
||||||
|
const newProgressDiv = document.createElement('div');
|
||||||
|
newProgressDiv.className = 'console-line render-progress';
|
||||||
|
newProgressDiv.style.color = '#00ff00';
|
||||||
|
newProgressDiv.textContent = `[系统] 渲染进度: ${progress}%`;
|
||||||
|
if (this.container.firstChild) {
|
||||||
|
this.container.insertBefore(newProgressDiv, this.container.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染一批数据
|
||||||
|
const renderBatch = () => {
|
||||||
|
const startIdx = currentBatch * batchSize;
|
||||||
|
const endIdx = Math.min((currentBatch + 1) * batchSize, total);
|
||||||
|
|
||||||
|
// 创建批量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) {
|
||||||
|
// 保留一个提示,避免完全空白
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'console-line';
|
||||||
|
placeholder.textContent = '[系统] 正在加载日志...';
|
||||||
|
placeholder.style.opacity = '0.5';
|
||||||
|
this.container.replaceChildren(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加新批次
|
||||||
|
this.container.appendChild(batchFragment);
|
||||||
|
|
||||||
|
currentBatch++;
|
||||||
|
showProgress();
|
||||||
|
|
||||||
|
// 继续下一批或完成
|
||||||
|
if (currentBatch < totalBatches) {
|
||||||
|
requestAnimationFrame(renderBatch);
|
||||||
|
} else {
|
||||||
|
// 渲染完成,清理进度提示
|
||||||
|
const progressDiv = this.container.querySelector('.render-progress');
|
||||||
|
if (progressDiv) {
|
||||||
|
progressDiv.remove();
|
||||||
|
}
|
||||||
|
console.log(`[渐进式渲染] 完成,共渲染 ${total} 行`);
|
||||||
|
this.isProgressiveRendering = false;
|
||||||
|
|
||||||
|
// 完成后触发滚动
|
||||||
|
if (this.autoScrollEnabled) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始渲染第一批
|
||||||
|
showProgress();
|
||||||
|
requestAnimationFrame(renderBatch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_ENDPOINT = '/api/config';
|
const CONFIG_ENDPOINT = '/api/config';
|
||||||
@@ -3507,8 +3823,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载论坛日志
|
// 加载论坛日志
|
||||||
|
let forumLogPosition = 0; // 记录已接收的日志位置
|
||||||
|
|
||||||
function loadForumLog() {
|
function loadForumLog() {
|
||||||
fetch('/api/forum/log')
|
// 【优化】使用历史API获取完整日志
|
||||||
|
fetch('/api/forum/log/history', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
position: 0, // 从头开始获取所有历史
|
||||||
|
max_lines: 5000 // 获取最近5000行历史
|
||||||
|
})
|
||||||
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// 【FIX Bug #5】检查是否仍然在forum页面
|
// 【FIX Bug #5】检查是否仍然在forum页面
|
||||||
@@ -3527,35 +3855,59 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatArea = document.getElementById('forumChatArea');
|
|
||||||
if (chatArea) {
|
|
||||||
chatArea.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const logLines = data.log_lines || [];
|
const logLines = data.log_lines || [];
|
||||||
const parsedMessages = data.parsed_messages || [];
|
forumLogPosition = data.position || 0; // 记录当前位置
|
||||||
|
|
||||||
|
// 清空并重新加载日志
|
||||||
if (logLines.length > 0) {
|
if (logLines.length > 0) {
|
||||||
clearConsoleLayer('forum', '[系统] Forum Engine 日志输出');
|
clearConsoleLayer('forum', '[系统] Forum Engine 历史日志');
|
||||||
logRenderers['forum'].render(); // 立即渲染清空提示
|
logRenderers['forum'].render(); // 立即渲染清空提示
|
||||||
logLines.forEach(line => appendConsoleTextLine('forum', line));
|
|
||||||
|
// 批量添加历史日志,避免卡顿
|
||||||
|
const batchSize = 100;
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
function addBatch() {
|
||||||
|
const batch = logLines.slice(index, index + batchSize);
|
||||||
|
batch.forEach(line => appendConsoleTextLine('forum', line));
|
||||||
|
index += batchSize;
|
||||||
|
|
||||||
|
if (index < logLines.length && currentApp === 'forum') {
|
||||||
|
requestAnimationFrame(addBatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addBatch();
|
||||||
} else {
|
} else {
|
||||||
forumLogLineCount = 0;
|
clearConsoleLayer('forum', '[系统] Forum Engine 暂无日志');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedMessages.length > 0) {
|
// 同时获取解析的消息(用于聊天区域)
|
||||||
parsedMessages.forEach(message => addForumMessage(message));
|
fetch('/api/forum/log')
|
||||||
}
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) return;
|
||||||
|
|
||||||
forumLogLineCount = logLines.length;
|
const chatArea = document.getElementById('forumChatArea');
|
||||||
|
if (chatArea) {
|
||||||
|
chatArea.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedMessages = data.parsed_messages || [];
|
||||||
|
if (parsedMessages.length > 0) {
|
||||||
|
parsedMessages.forEach(message => addForumMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
forumLogLineCount = data.log_lines ? data.log_lines.length : 0;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('加载论坛日志失败:', error);
|
console.error('加载论坛历史日志失败:', error);
|
||||||
// 【优化】显示错误提示
|
// 【优化】显示错误提示
|
||||||
if (currentApp === 'forum') {
|
if (currentApp === 'forum') {
|
||||||
const renderer = logRenderers['forum'];
|
const renderer = logRenderers['forum'];
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer.clear('[错误] 加载Forum日志失败: ' + error.message);
|
renderer.clear('[错误] 加载Forum历史日志失败: ' + error.message);
|
||||||
renderer.render();
|
renderer.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user