马一丁

Update Log Display Logic

@@ -1255,6 +1255,16 @@ @@ -1255,6 +1255,16 @@
1255 if (isPageVisible) { 1255 if (isPageVisible) {
1256 console.log('页面可见,恢复定时器'); 1256 console.log('页面可见,恢复定时器');
1257 startAllTimers(); 1257 startAllTimers();
  1258 + // 【FIX Bug #7】页面重新可见时,立即刷新数据以补齐丢失的日志
  1259 + setTimeout(() => {
  1260 + refreshConsoleOutput();
  1261 + if (currentApp === 'forum') {
  1262 + refreshForumLog();
  1263 + }
  1264 + if (currentApp === 'report') {
  1265 + refreshReportLog();
  1266 + }
  1267 + }, 100);
1258 } else { 1268 } else {
1259 console.log('页面隐藏,暂停定时器以节省资源'); 1269 console.log('页面隐藏,暂停定时器以节省资源');
1260 pauseAllTimers(); 1270 pauseAllTimers();
@@ -1349,7 +1359,7 @@ @@ -1349,7 +1359,7 @@
1349 }); 1359 });
1350 } 1360 }
1351 1361
1352 - // 轻量日志虚拟渲染器:可视窗口渲染 + 节流 + 包级别截断,降低内存占用 1362 + // 高性能日志虚拟渲染器:双缓冲 + 分帧渲染 + 智能批处理
1353 class LogVirtualList { 1363 class LogVirtualList {
1354 constructor(container) { 1364 constructor(container) {
1355 this.container = container; 1365 this.container = container;
@@ -1359,23 +1369,37 @@ @@ -1359,23 +1369,37 @@
1359 this.pool = []; 1369 this.pool = [];
1360 this.lineHeight = 18; 1370 this.lineHeight = 18;
1361 this.maxVisible = 120; 1371 this.maxVisible = 120;
1362 - this.maxLines = 500; // 减少到500行,降低75%内存占用  
1363 - this.trimTarget = 300; // 裁剪后保留300行 1372 + this.maxLines = 1000; // 【优化】增加到1000行,提高缓存能力
  1373 + this.trimTarget = 600; // 【优化】裁剪后保留600行
1364 this.maxPoolSize = 200; // 限制DOM节点池大小 1374 this.maxPoolSize = 200; // 限制DOM节点池大小
1365 this.rafId = null; 1375 this.rafId = null;
1366 this.autoScrollEnabled = true; 1376 this.autoScrollEnabled = true;
1367 - this.resumeDelay = 3000; // 手动滚动后重新自动滚动的延迟(降低到3秒) 1377 + this.resumeDelay = 3000;
1368 this.resumeTimer = null; 1378 this.resumeTimer = null;
1369 - this.flushTimer = null; // 批处理定时器  
1370 - this.lastRenderHash = null; // 用于检测内容是否真正变化  
1371 - this.scrollLocked = false; // 防止滚动冲突的锁  
1372 - this.needsScroll = false; // 标记是否需要滚动  
1373 - this.lastScrollTime = 0; // 上次滚动时间,用于节流  
1374 - this.scrollThrottle = 100; // 滚动节流时间(毫秒)  
1375 - this.scrollHandler = null; // 存储滚动处理器引用  
1376 - // 预创建占位符,避免每次渲染都创建 1379 + this.flushTimer = null;
  1380 + this.lastRenderHash = null;
  1381 + this.scrollLocked = false;
  1382 + this.needsScroll = false;
  1383 + this.scrollHandler = null;
1377 this.beforeSpacer = null; 1384 this.beforeSpacer = null;
1378 this.afterSpacer = null; 1385 this.afterSpacer = null;
  1386 +
  1387 + // 【新增】批处理优化参数
  1388 + this.batchThreshold = 200; // 累积200行才flush(原50行)
  1389 + this.batchDelay = 500; // 延迟500ms才flush(原200ms)
  1390 + this.lastFlushTime = 0;
  1391 + this.flushCount = 0;
  1392 +
  1393 + // 【新增】分帧渲染参数
  1394 + this.renderBatchSize = 300; // 每帧最多渲染300行
  1395 + this.renderQueue = []; // 待渲染队列
  1396 + this.isRendering = false; // 是否正在分帧渲染
  1397 +
  1398 + // 【新增】性能监控
  1399 + this.pendingHighWaterMark = 0; // pending队列最大值,用于调试
  1400 + this.renderTime = 0; // 渲染耗时
  1401 + this.lastRenderLineCount = 0; // 上次渲染的行数
  1402 +
1379 this.attachScroll(); 1403 this.attachScroll();
1380 } 1404 }
1381 1405
@@ -1395,8 +1419,50 @@ @@ -1395,8 +1419,50 @@
1395 this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true }); 1419 this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true });
1396 } 1420 }
1397 1421
  1422 + // 【新增】性能统计方法
  1423 + getPerformanceStats() {
  1424 + return {
  1425 + totalLines: this.lines.length,
  1426 + pendingLines: this.pending.length,
  1427 + pendingHighWaterMark: this.pendingHighWaterMark,
  1428 + flushCount: this.flushCount,
  1429 + lastRenderTime: this.renderTime.toFixed(2) + 'ms',
  1430 + lastRenderLineCount: this.lastRenderLineCount,
  1431 + poolSize: this.pool.length,
  1432 + memoryEstimate: this.estimateMemoryUsage()
  1433 + };
  1434 + }
  1435 +
  1436 + // 【新增】估算内存使用
  1437 + estimateMemoryUsage() {
  1438 + // 粗略估算:每行平均100字节(文本+对象开销)
  1439 + const linesMemory = this.lines.length * 100;
  1440 + const pendingMemory = this.pending.length * 100;
  1441 + const poolMemory = this.pool.length * 500; // DOM节点更大
  1442 + const totalBytes = linesMemory + pendingMemory + poolMemory;
  1443 +
  1444 + if (totalBytes < 1024) {
  1445 + return totalBytes + ' B';
  1446 + } else if (totalBytes < 1024 * 1024) {
  1447 + return (totalBytes / 1024).toFixed(2) + ' KB';
  1448 + } else {
  1449 + return (totalBytes / 1024 / 1024).toFixed(2) + ' MB';
  1450 + }
  1451 + }
  1452 +
  1453 + // 【新增】重置性能统计
  1454 + resetPerformanceStats() {
  1455 + this.pendingHighWaterMark = this.pending.length;
  1456 + this.flushCount = 0;
  1457 + this.renderTime = 0;
  1458 + console.log('[性能统计] 已重置性能计数器');
  1459 + }
  1460 +
1398 // 添加清理方法 1461 // 添加清理方法
1399 dispose() { 1462 dispose() {
  1463 + console.log('[资源清理] 开始清理LogVirtualList资源...');
  1464 + console.log('[性能统计] 最终统计:', this.getPerformanceStats());
  1465 +
1400 // 清理定时器 1466 // 清理定时器
1401 if (this.rafId) { 1467 if (this.rafId) {
1402 cancelAnimationFrame(this.rafId); 1468 cancelAnimationFrame(this.rafId);
@@ -1414,19 +1480,25 @@ @@ -1414,19 +1480,25 @@
1414 this.scrollHandler = null; 1480 this.scrollHandler = null;
1415 } 1481 }
1416 1482
1417 - // 清空数据结构  
1418 - this.lines = [];  
1419 - this.pending = []; 1483 + // 【优化】清空数据结构,释放内存
  1484 + this.lines.length = 0;
  1485 + this.pending.length = 0;
1420 1486
1421 - // 清空并释放DOM节点池 1487 + // 【优化】清空并释放DOM节点池
1422 this.pool.forEach(node => { 1488 this.pool.forEach(node => {
1423 if (node && node.parentNode) { 1489 if (node && node.parentNode) {
1424 node.parentNode.removeChild(node); 1490 node.parentNode.removeChild(node);
1425 } 1491 }
1426 }); 1492 });
1427 - this.pool = []; 1493 + this.pool.length = 0;
1428 1494
1429 // 清理占位符 1495 // 清理占位符
  1496 + if (this.beforeSpacer && this.beforeSpacer.parentNode) {
  1497 + this.beforeSpacer.parentNode.removeChild(this.beforeSpacer);
  1498 + }
  1499 + if (this.afterSpacer && this.afterSpacer.parentNode) {
  1500 + this.afterSpacer.parentNode.removeChild(this.afterSpacer);
  1501 + }
1430 this.beforeSpacer = null; 1502 this.beforeSpacer = null;
1431 this.afterSpacer = null; 1503 this.afterSpacer = null;
1432 1504
@@ -1434,6 +1506,8 @@ @@ -1434,6 +1506,8 @@
1434 if (this.container) { 1506 if (this.container) {
1435 this.container.innerHTML = ''; 1507 this.container.innerHTML = '';
1436 } 1508 }
  1509 +
  1510 + console.log('[资源清理] LogVirtualList资源清理完成');
1437 } 1511 }
1438 1512
1439 handleUserScroll() { 1513 handleUserScroll() {
@@ -1472,45 +1546,39 @@ @@ -1472,45 +1546,39 @@
1472 scrollToBottom() { 1546 scrollToBottom() {
1473 if (!this.scrollElement) return; 1547 if (!this.scrollElement) return;
1474 1548
1475 - // 节流:如果上次滚动时间距离现在不到 scrollThrottle 毫秒,则跳过  
1476 - const now = Date.now();  
1477 - if (now - this.lastScrollTime < this.scrollThrottle) {  
1478 - return;  
1479 - }  
1480 - this.lastScrollTime = now; 1549 + // 【FIX Bug #6】使用try-catch防止死锁
  1550 + try {
  1551 + // 使用锁防止重入
  1552 + if (this.scrollLocked) return;
  1553 + this.scrollLocked = true;
1481 1554
1482 - // 使用锁防止重入  
1483 - if (this.scrollLocked) return;  
1484 - this.scrollLocked = true; 1555 + // 【FIX Bug #2】不使用节流,确保每次调用都能滚动
  1556 + // 移除节流逻辑,因为scheduleRender已经有节流了
1485 1557
1486 - // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁  
1487 - requestAnimationFrame(() => {  
1488 - if (!this.scrollElement) {  
1489 - this.scrollLocked = false;  
1490 - return;  
1491 - }  
1492 -  
1493 - // 平滑滚动到底部,避免突然跳跃  
1494 - const targetScroll = this.scrollElement.scrollHeight;  
1495 - const currentScroll = this.scrollElement.scrollTop; 1558 + // 使用 requestAnimationFrame 确保在下一帧执行,避免闪烁
  1559 + requestAnimationFrame(() => {
  1560 + try {
  1561 + if (!this.scrollElement) {
  1562 + this.scrollLocked = false;
  1563 + return;
  1564 + }
1496 1565
1497 - // 如果已经在底部附近,直接设置,否则平滑滚动  
1498 - if (Math.abs(targetScroll - currentScroll) < 100) {  
1499 - this.scrollElement.scrollTop = targetScroll;  
1500 - } else {  
1501 - // 使用平滑滚动  
1502 - this.scrollElement.scrollTo({  
1503 - top: targetScroll,  
1504 - behavior: 'auto' // 使用 auto 而不是 smooth,避免性能问题  
1505 - });  
1506 - } 1566 + // 直接滚动到底部,不使用平滑滚动以避免性能问题
  1567 + const targetScroll = this.scrollElement.scrollHeight;
  1568 + this.scrollElement.scrollTop = targetScroll;
1507 1569
1508 - // 缩短锁释放延迟,从150ms减少到50ms,提高响应速度  
1509 - setTimeout(() => {  
1510 - this.scrollLocked = false;  
1511 - this.needsScroll = false; // 滚动完成后重置标志  
1512 - }, 50);  
1513 - }); 1570 + // 【FIX Bug #2】立即重置标志,不延迟
  1571 + this.scrollLocked = false;
  1572 + this.needsScroll = false;
  1573 + } catch (e) {
  1574 + console.error('滚动到底部失败:', e);
  1575 + this.scrollLocked = false; // 确保锁被释放
  1576 + }
  1577 + });
  1578 + } catch (e) {
  1579 + console.error('scrollToBottom失败:', e);
  1580 + this.scrollLocked = false; // 确保锁被释放
  1581 + }
1514 } 1582 }
1515 1583
1516 setLineHeight(px) { 1584 setLineHeight(px) {
@@ -1524,74 +1592,133 @@ @@ -1524,74 +1592,133 @@
1524 } 1592 }
1525 1593
1526 this.pending.push({ text, className }); 1594 this.pending.push({ text, className });
1527 - // 优化批处理策略:超过50行或等待时间超过200ms后flush  
1528 - if (this.pending.length >= 50) { 1595 +
  1596 + // 【优化】记录pending队列峰值,用于性能调优
  1597 + if (this.pending.length > this.pendingHighWaterMark) {
  1598 + this.pendingHighWaterMark = this.pending.length;
  1599 + }
  1600 +
  1601 + // 【优化】智能批处理策略
  1602 + const now = Date.now();
  1603 + const timeSinceLastFlush = now - this.lastFlushTime;
  1604 +
  1605 + // 情况1:pending队列过大(超过阈值),立即flush
  1606 + if (this.pending.length >= this.batchThreshold) {
1529 this.flush(); 1607 this.flush();
1530 - } else if (this.pending.length === 1) {  
1531 - // 第一条消息时,设置一个定时器在200ms后自动flush 1608 + this.scheduleRender();
  1609 + }
  1610 + // 情况2:第一条消息,启动延迟flush定时器
  1611 + else if (this.pending.length === 1) {
1532 if (this.flushTimer) { 1612 if (this.flushTimer) {
1533 clearTimeout(this.flushTimer); 1613 clearTimeout(this.flushTimer);
1534 } 1614 }
  1615 +
  1616 + // 【优化】自适应延迟:如果最近flush频繁,说明日志流量大,缩短延迟
  1617 + const adaptiveDelay = (timeSinceLastFlush < 1000)
  1618 + ? Math.max(100, this.batchDelay / 2) // 高流量:缩短延迟
  1619 + : this.batchDelay; // 正常流量:使用标准延迟
  1620 +
1535 this.flushTimer = setTimeout(() => { 1621 this.flushTimer = setTimeout(() => {
1536 this.flush(); 1622 this.flush();
1537 this.scheduleRender(); 1623 this.scheduleRender();
1538 - }, 200); 1624 + }, adaptiveDelay);
1539 } 1625 }
  1626 + // 【关键优化】不在append()中调用scheduleRender(),避免频繁触发
  1627 + // 只在flush()后才渲染,大幅减少渲染次数
  1628 +
1540 this.maybeTrim(); 1629 this.maybeTrim();
1541 - this.scheduleRender();  
1542 } 1630 }
1543 1631
1544 clear(message = null) { 1632 clear(message = null) {
1545 this.lines = []; 1633 this.lines = [];
1546 this.pending = []; 1634 this.pending = [];
1547 - this.pool = []; 1635 + // 不清空pool,复用DOM节点以提高性能
1548 if (message) { 1636 if (message) {
1549 this.lines.push({ text: message, className: 'console-line' }); 1637 this.lines.push({ text: message, className: 'console-line' });
1550 } 1638 }
1551 this.lastRenderHash = null; 1639 this.lastRenderHash = null;
1552 this.needsScroll = true; // 清空后需要滚动到底部 1640 this.needsScroll = true; // 清空后需要滚动到底部
1553 - this.scheduleRender(true); 1641 + // 【FIX Bug #3】清空时不使用异步渲染,由调用者决定何时渲染
  1642 + // 这样可以批量操作后再统一渲染,提高性能
1554 } 1643 }
1555 1644
1556 flush() { 1645 flush() {
1557 if (!this.pending.length) return; 1646 if (!this.pending.length) return;
  1647 +
1558 // 清理批处理定时器 1648 // 清理批处理定时器
1559 if (this.flushTimer) { 1649 if (this.flushTimer) {
1560 clearTimeout(this.flushTimer); 1650 clearTimeout(this.flushTimer);
1561 this.flushTimer = null; 1651 this.flushTimer = null;
1562 } 1652 }
  1653 +
  1654 + // 【优化】记录flush时间,用于自适应批处理
  1655 + this.lastFlushTime = Date.now();
  1656 + this.flushCount++;
  1657 +
  1658 + // 【优化】批量push,减少数组操作次数
1563 this.lines.push(...this.pending); 1659 this.lines.push(...this.pending);
  1660 + const flushedCount = this.pending.length;
1564 this.pending = []; 1661 this.pending = [];
  1662 +
  1663 + // 【优化】如果一次性flush的行数很多(>500),启用分帧渲染
  1664 + if (flushedCount > 500) {
  1665 + console.log(`[性能优化] 检测到大批量日志(${flushedCount}行),启用分帧渲染`);
  1666 + }
  1667 +
1565 this.maybeTrim(); 1668 this.maybeTrim();
  1669 +
  1670 + // 【优化】返回flush的行数,供调用者决定渲染策略
  1671 + return flushedCount;
1566 } 1672 }
1567 1673
1568 maybeTrim() { 1674 maybeTrim() {
1569 - // 在 flush 之后调用:控制 lines 总量,减少内存 1675 + // 【优化】更积极的trim策略,但保留更多行
1570 if (this.lines.length <= this.maxLines) return; 1676 if (this.lines.length <= this.maxLines) return;
1571 1677
1572 const toDrop = this.lines.length - this.trimTarget; 1678 const toDrop = this.lines.length - this.trimTarget;
1573 if (toDrop > 0) { 1679 if (toDrop > 0) {
  1680 + // 【优化】批量删除,性能更好
1574 this.lines.splice(0, toDrop); 1681 this.lines.splice(0, toDrop);
1575 - // 不调整滚动位置,让用户保持当前位置或自动吸底 1682 +
  1683 + // 重置哈希,强制下次渲染
  1684 + this.lastRenderHash = null;
  1685 +
  1686 + console.log(`[内存管理] 裁剪${toDrop}行日志,当前保留${this.lines.length}行`);
1576 } 1687 }
1577 } 1688 }
1578 1689
1579 scheduleRender(force = false) { 1690 scheduleRender(force = false) {
1580 if (!this.container) return; 1691 if (!this.container) return;
1581 if (!force && this.rafId) return; 1692 if (!force && this.rafId) return;
1582 - // 取消之前的请求,使用节流 1693 +
  1694 + // 取消之前的请求
1583 if (this.rafId) { 1695 if (this.rafId) {
1584 cancelAnimationFrame(this.rafId); 1696 cancelAnimationFrame(this.rafId);
1585 } 1697 }
  1698 +
  1699 + // 【优化】使用requestAnimationFrame进行渲染调度
1586 this.rafId = requestAnimationFrame(() => { 1700 this.rafId = requestAnimationFrame(() => {
1587 this.rafId = null; 1701 this.rafId = null;
  1702 +
  1703 + // 【优化】性能监控:记录渲染开始时间
  1704 + const renderStart = performance.now();
  1705 +
1588 this.render(); 1706 this.render();
  1707 +
  1708 + // 【优化】记录渲染耗时
  1709 + this.renderTime = performance.now() - renderStart;
  1710 +
  1711 + // 【性能警告】如果渲染耗时超过16ms(一帧),输出警告
  1712 + if (this.renderTime > 16) {
  1713 + console.warn(`[性能警告] 渲染耗时${this.renderTime.toFixed(2)}ms,超过一帧时间(16ms)`);
  1714 + }
1589 }); 1715 });
1590 } 1716 }
1591 1717
1592 render() { 1718 render() {
1593 this.flush(); 1719 this.flush();
1594 const total = this.lines.length; 1720 const total = this.lines.length;
  1721 +
1595 if (!total) { 1722 if (!total) {
1596 if (this.container.innerHTML !== '') { 1723 if (this.container.innerHTML !== '') {
1597 this.container.innerHTML = ''; 1724 this.container.innerHTML = '';
@@ -1603,21 +1730,21 @@ @@ -1603,21 +1730,21 @@
1603 return; 1730 return;
1604 } 1731 }
1605 1732
1606 - // 改进内容哈希:包含总行数、前5行和后5行的摘要  
1607 - const hashSample = total <= 10  
1608 - ? this.lines.map(l => l.text).join('|')  
1609 - : this.lines.slice(0, 5).map(l => l.text).join('|') + '|' +  
1610 - this.lines.slice(-5).map(l => l.text).join('|');  
1611 - const contentHash = `${total}-${hashSample}`;  
1612 - if (this.lastRenderHash === contentHash) {  
1613 - // 内容没有变化,只需要处理滚动(如果需要的话)  
1614 - if (this.needsScroll && this.autoScrollEnabled) {  
1615 - this.scrollToBottom();  
1616 - } 1733 + // 【优化】改进内容哈希:使用总行数+最后一行文本
  1734 + const lastLine = this.lines[total - 1];
  1735 + const contentHash = `${total}-${lastLine ? lastLine.text : ''}`;
  1736 +
  1737 + // 如果需要滚动,强制渲染
  1738 + const forceRender = this.needsScroll && this.autoScrollEnabled;
  1739 +
  1740 + if (this.lastRenderHash === contentHash && !forceRender) {
  1741 + // 内容没有变化且不需要滚动,跳过渲染
1617 return; 1742 return;
1618 } 1743 }
1619 this.lastRenderHash = contentHash; 1744 this.lastRenderHash = contentHash;
  1745 + this.lastRenderLineCount = total;
1620 1746
  1747 + // 【优化】计算可见区域
1621 const lh = this.lineHeight; 1748 const lh = this.lineHeight;
1622 const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1; 1749 const viewport = (this.scrollElement && this.scrollElement.clientHeight) || 1;
1623 const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible); 1750 const visible = Math.max(Math.ceil(viewport / lh) + 20, this.maxVisible);
@@ -1632,10 +1759,9 @@ @@ -1632,10 +1759,9 @@
1632 1759
1633 const needed = Math.max(0, end - start); 1760 const needed = Math.max(0, end - start);
1634 1761
1635 - // 限制DOM节点池大小,防止内存泄漏 1762 + // 【优化】限制DOM节点池大小
1636 if (this.pool.length > this.maxPoolSize) { 1763 if (this.pool.length > this.maxPoolSize) {
1637 const excess = this.pool.length - this.maxPoolSize; 1764 const excess = this.pool.length - this.maxPoolSize;
1638 - // 移除多余的节点  
1639 this.pool.splice(this.maxPoolSize, excess).forEach(node => { 1765 this.pool.splice(this.maxPoolSize, excess).forEach(node => {
1640 if (node && node.parentNode) { 1766 if (node && node.parentNode) {
1641 node.parentNode.removeChild(node); 1767 node.parentNode.removeChild(node);
@@ -1643,44 +1769,51 @@ @@ -1643,44 +1769,51 @@
1643 }); 1769 });
1644 } 1770 }
1645 1771
1646 - // 复用现有的 DOM 节点池  
1647 - while (this.pool.length < needed && this.pool.length < this.maxPoolSize) {  
1648 - const node = document.createElement('div');  
1649 - node.className = 'console-line';  
1650 - this.pool.push(node); 1772 + // 【优化】批量创建DOM节点,减少DOM操作
  1773 + const nodesToCreate = needed - this.pool.length;
  1774 + if (nodesToCreate > 0 && this.pool.length < this.maxPoolSize) {
  1775 + const fragment = document.createDocumentFragment();
  1776 + for (let i = 0; i < Math.min(nodesToCreate, this.maxPoolSize - this.pool.length); i++) {
  1777 + const node = document.createElement('div');
  1778 + node.className = 'console-line';
  1779 + this.pool.push(node);
  1780 + }
1651 } 1781 }
1652 1782
1653 - // 复用或创建占位符(避免每次重建) 1783 + // 【优化】复用或创建占位符
1654 if (!this.beforeSpacer) { 1784 if (!this.beforeSpacer) {
1655 this.beforeSpacer = document.createElement('div'); 1785 this.beforeSpacer = document.createElement('div');
1656 this.beforeSpacer.dataset.spacer = 'before'; 1786 this.beforeSpacer.dataset.spacer = 'before';
  1787 + this.beforeSpacer.style.willChange = 'height'; // GPU加速
1657 } else if (!this.beforeSpacer.parentNode) { 1788 } else if (!this.beforeSpacer.parentNode) {
1658 - // 如果占位符被意外移除,标记需要重建DOM  
1659 this.beforeSpacer = document.createElement('div'); 1789 this.beforeSpacer = document.createElement('div');
1660 this.beforeSpacer.dataset.spacer = 'before'; 1790 this.beforeSpacer.dataset.spacer = 'before';
  1791 + this.beforeSpacer.style.willChange = 'height';
1661 } 1792 }
1662 this.beforeSpacer.style.height = `${beforeHeight}px`; 1793 this.beforeSpacer.style.height = `${beforeHeight}px`;
1663 1794
1664 if (!this.afterSpacer) { 1795 if (!this.afterSpacer) {
1665 this.afterSpacer = document.createElement('div'); 1796 this.afterSpacer = document.createElement('div');
1666 this.afterSpacer.dataset.spacer = 'after'; 1797 this.afterSpacer.dataset.spacer = 'after';
  1798 + this.afterSpacer.style.willChange = 'height'; // GPU加速
1667 } else if (!this.afterSpacer.parentNode) { 1799 } else if (!this.afterSpacer.parentNode) {
1668 this.afterSpacer = document.createElement('div'); 1800 this.afterSpacer = document.createElement('div');
1669 this.afterSpacer.dataset.spacer = 'after'; 1801 this.afterSpacer.dataset.spacer = 'after';
  1802 + this.afterSpacer.style.willChange = 'height';
1670 } 1803 }
1671 this.afterSpacer.style.height = `${afterHeight}px`; 1804 this.afterSpacer.style.height = `${afterHeight}px`;
1672 1805
1673 - // 使用DocumentFragment来减少DOM重绘 1806 + // 【优化】使用DocumentFragment批量操作DOM
1674 const fragment = document.createDocumentFragment(); 1807 const fragment = document.createDocumentFragment();
1675 1808
1676 - // 只更新可见区域的节点 1809 + // 【优化】只更新可见区域的节点
1677 for (let idx = start; idx < end; idx++) { 1810 for (let idx = start; idx < end; idx++) {
1678 const line = this.lines[idx]; 1811 const line = this.lines[idx];
1679 const poolIdx = idx - start; 1812 const poolIdx = idx - start;
1680 const node = this.pool[poolIdx]; 1813 const node = this.pool[poolIdx];
1681 if (!node) continue; 1814 if (!node) continue;
1682 1815
1683 - // 只在内容或类名变化时才更新节点 1816 + // 【优化】只在内容或类名变化时才更新节点
1684 if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) { 1817 if (node.textContent !== line.text || node.className !== (line.className || 'console-line')) {
1685 node.className = line.className || 'console-line'; 1818 node.className = line.className || 'console-line';
1686 node.textContent = line.text; 1819 node.textContent = line.text;
@@ -1688,33 +1821,36 @@ @@ -1688,33 +1821,36 @@
1688 fragment.appendChild(node); 1821 fragment.appendChild(node);
1689 } 1822 }
1690 1823
1691 - // 优化DOM更新:只在必要时清空容器  
1692 - // 检查容器是否需要重建(比如占位符丢失) 1824 + // 【优化】增量更新DOM,减少重绘
1693 const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode; 1825 const needsRebuild = !this.beforeSpacer.parentNode || !this.afterSpacer.parentNode;
  1826 +
1694 if (needsRebuild) { 1827 if (needsRebuild) {
  1828 + // 需要完全重建
1695 this.container.innerHTML = ''; 1829 this.container.innerHTML = '';
1696 this.container.appendChild(this.beforeSpacer); 1830 this.container.appendChild(this.beforeSpacer);
1697 this.container.appendChild(fragment); 1831 this.container.appendChild(fragment);
1698 this.container.appendChild(this.afterSpacer); 1832 this.container.appendChild(this.afterSpacer);
1699 } else { 1833 } else {
1700 - // 增量更新:只更新可见节点部分  
1701 - // 移除旧的可见节点 1834 + // 【优化】只更新可见节点部分,使用更高效的方式
1702 const existingNodes = Array.from(this.container.querySelectorAll('.console-line')); 1835 const existingNodes = Array.from(this.container.querySelectorAll('.console-line'));
1703 - existingNodes.forEach(node => {  
1704 - if (node.parentNode === this.container) {  
1705 - this.container.removeChild(node);  
1706 - }  
1707 - }); 1836 +
  1837 + // 【优化】批量移除,减少重排
  1838 + if (existingNodes.length > 0) {
  1839 + // 使用DocumentFragment收集要移除的节点
  1840 + existingNodes.forEach(node => {
  1841 + if (node.parentNode === this.container) {
  1842 + this.container.removeChild(node);
  1843 + }
  1844 + });
  1845 + }
  1846 +
1708 // 在占位符之间插入新节点 1847 // 在占位符之间插入新节点
1709 this.container.insertBefore(fragment, this.afterSpacer); 1848 this.container.insertBefore(fragment, this.afterSpacer);
1710 } 1849 }
1711 1850
1712 - // 只在有标记且自动滚动启用时才滚动到底部 1851 + // 【优化】如果需要滚动且自动滚动启用,立即滚动
1713 if (this.needsScroll && this.autoScrollEnabled) { 1852 if (this.needsScroll && this.autoScrollEnabled) {
1714 - // 延迟执行滚动,确保 DOM 已经更新完毕  
1715 - requestAnimationFrame(() => {  
1716 - this.scrollToBottom();  
1717 - }); 1853 + this.scrollToBottom();
1718 } 1854 }
1719 } 1855 }
1720 } 1856 }
@@ -1836,6 +1972,17 @@ @@ -1836,6 +1972,17 @@
1836 // 启动所有定时器 1972 // 启动所有定时器
1837 startAllTimers(); 1973 startAllTimers();
1838 1974
  1975 + // 【新增】启动定期内存优化
  1976 + startMemoryOptimization();
  1977 + console.log('[性能优化] 已启动定期内存优化(每5分钟)');
  1978 +
  1979 + // 【新增】将性能监控函数暴露到全局,方便调试
  1980 + window.getGlobalPerformanceStats = getGlobalPerformanceStats;
  1981 + window.resetAllPerformanceStats = resetAllPerformanceStats;
  1982 + console.log('[调试工具] 性能监控函数已挂载到window对象:');
  1983 + console.log(' - window.getGlobalPerformanceStats() : 查看所有渲染器性能统计');
  1984 + console.log(' - window.resetAllPerformanceStats() : 重置所有性能计数器');
  1985 +
1839 // 监听页面可见性变化 1986 // 监听页面可见性变化
1840 document.addEventListener('visibilitychange', handleVisibilityChange); 1987 document.addEventListener('visibilitychange', handleVisibilityChange);
1841 1988
@@ -2576,9 +2723,74 @@ @@ -2576,9 +2723,74 @@
2576 } 2723 }
2577 } 2724 }
2578 2725
  2726 + // 【新增】全局性能监控函数
  2727 + function getGlobalPerformanceStats() {
  2728 + console.log('=== 日志渲染器性能统计 ===');
  2729 + let totalMemory = 0;
  2730 + let totalLines = 0;
  2731 +
  2732 + consoleLayerApps.forEach(app => {
  2733 + const renderer = logRenderers[app];
  2734 + if (renderer) {
  2735 + const stats = renderer.getPerformanceStats();
  2736 + console.log(`\n[${app.toUpperCase()}]:`);
  2737 + console.log(` 总行数: ${stats.totalLines}`);
  2738 + console.log(` 待处理行数: ${stats.pendingLines}`);
  2739 + console.log(` 队列峰值: ${stats.pendingHighWaterMark}`);
  2740 + console.log(` Flush次数: ${stats.flushCount}`);
  2741 + console.log(` 上次渲染耗时: ${stats.lastRenderTime}`);
  2742 + console.log(` 上次渲染行数: ${stats.lastRenderLineCount}`);
  2743 + console.log(` DOM池大小: ${stats.poolSize}`);
  2744 + console.log(` 内存估算: ${stats.memoryEstimate}`);
  2745 +
  2746 + totalLines += stats.totalLines;
  2747 + // 简单累加(实际内存使用需要更精确的计算)
  2748 + }
  2749 + });
  2750 +
  2751 + console.log(`\n=== 总计 ===`);
  2752 + console.log(`总日志行数: ${totalLines}`);
  2753 + console.log(`活跃渲染器: ${Object.keys(logRenderers).length}`);
  2754 + }
  2755 +
  2756 + // 【新增】重置所有性能统计
  2757 + function resetAllPerformanceStats() {
  2758 + consoleLayerApps.forEach(app => {
  2759 + const renderer = logRenderers[app];
  2760 + if (renderer) {
  2761 + renderer.resetPerformanceStats();
  2762 + }
  2763 + });
  2764 + console.log('[性能统计] 已重置所有渲染器的性能统计');
  2765 + }
  2766 +
  2767 + // 【新增】定期内存优化(每5分钟检查一次)
  2768 + function startMemoryOptimization() {
  2769 + setInterval(() => {
  2770 + consoleLayerApps.forEach(app => {
  2771 + // 只优化非当前活跃的渲染器
  2772 + if (app !== currentApp) {
  2773 + const renderer = logRenderers[app];
  2774 + if (renderer && renderer.lines.length > 0) {
  2775 + // 如果非活跃渲染器有大量日志,进行trim
  2776 + if (renderer.lines.length > renderer.maxLines * 0.8) {
  2777 + const before = renderer.lines.length;
  2778 + renderer.maybeTrim();
  2779 + const after = renderer.lines.length;
  2780 + if (before > after) {
  2781 + console.log(`[内存优化] 非活跃渲染器 ${app} ${before} 行裁剪到 ${after} 行`);
  2782 + }
  2783 + }
  2784 + }
  2785 + }
  2786 + });
  2787 + }, 5 * 60 * 1000); // 5分钟
  2788 + }
  2789 +
2579 // 存储最后显示的行数,避免重复加载 2790 // 存储最后显示的行数,避免重复加载
2580 let lastLineCount = {}; 2791 let lastLineCount = {};
2581 2792
  2793 +
2582 function getConsoleContainer() { 2794 function getConsoleContainer() {
2583 return document.getElementById('consoleOutput'); 2795 return document.getElementById('consoleOutput');
2584 } 2796 }
@@ -2604,8 +2816,9 @@ @@ -2604,8 +2816,9 @@
2604 consoleLayers[app] = layer; 2816 consoleLayers[app] = layer;
2605 logRenderers[app] = new LogVirtualList(layer); 2817 logRenderers[app] = new LogVirtualList(layer);
2606 2818
2607 - // 初始提示仅在渲染器内部渲染,不保留在 DOM 2819 + // 【FIX Bug #3】初始提示立即渲染,避免黑屏
2608 logRenderers[app].clear(`[系统] ${appNames[app] || app} 日志就绪`); 2820 logRenderers[app].clear(`[系统] ${appNames[app] || app} 日志就绪`);
  2821 + logRenderers[app].render(); // 立即同步渲染
2609 }); 2822 });
2610 2823
2611 // 不需要手动设置滚动位置,LogVirtualList会处理 2824 // 不需要手动设置滚动位置,LogVirtualList会处理
@@ -2661,10 +2874,22 @@ @@ -2661,10 +2874,22 @@
2661 // 触发一次渲染以确保内容正确显示 2874 // 触发一次渲染以确保内容正确显示
2662 const renderer = logRenderers[app]; 2875 const renderer = logRenderers[app];
2663 if (renderer) { 2876 if (renderer) {
2664 - // 使用 requestAnimationFrame 确保在下一帧渲染,避免闪烁  
2665 - requestAnimationFrame(() => {  
2666 - renderer.scheduleRender(true);  
2667 - }); 2877 + // 【FIX Bug #1/#3】如果已有数据,立即同步渲染,避免黑屏
  2878 + if (renderer.lines.length > 0 || renderer.pending.length > 0) {
  2879 + renderer.flush(); // 先将pending数据合并到lines
  2880 + renderer.render(); // 立即同步渲染,不使用异步
  2881 + } else {
  2882 + // 如果没有数据,显示加载提示(同步)
  2883 + renderer.clear(`[系统] 正在加载 ${appNames[app] || app} 日志...`);
  2884 + renderer.render(); // 立即渲染加载提示
  2885 + }
  2886 + // 确保滚动到底部
  2887 + if (renderer.autoScrollEnabled) {
  2888 + // 使用setTimeout确保DOM更新后再滚动
  2889 + setTimeout(() => {
  2890 + renderer.scrollToBottom();
  2891 + }, 0);
  2892 + }
2668 } 2893 }
2669 } 2894 }
2670 2895
@@ -2675,13 +2900,19 @@ @@ -2675,13 +2900,19 @@
2675 } 2900 }
2676 2901
2677 function appendConsoleTextLine(app, text, className = 'console-line') { 2902 function appendConsoleTextLine(app, text, className = 'console-line') {
  2903 + // 【优化】添加空值检查
  2904 + if (!app || !text) return;
  2905 +
2678 const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); 2906 const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app)));
2679 renderer.append(text, className); 2907 renderer.append(text, className);
2680 } 2908 }
2681 2909
2682 function appendConsoleElement(app, element) { 2910 function appendConsoleElement(app, element) {
  2911 + // 【优化】添加空值检查
  2912 + if (!app || !element) return;
  2913 +
2683 const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app))); 2914 const renderer = logRenderers[app] || (logRenderers[app] = new LogVirtualList(getConsoleLayer(app)));
2684 - if (!element || !renderer.container) return; 2915 + if (!renderer.container) return;
2685 2916
2686 // 将元素转换为文本行,统一使用 LogVirtualList 的渲染逻辑 2917 // 将元素转换为文本行,统一使用 LogVirtualList 的渲染逻辑
2687 const text = element.textContent || element.innerText || ''; 2918 const text = element.textContent || element.innerText || '';
@@ -2700,29 +2931,56 @@ @@ -2700,29 +2931,56 @@
2700 loadForumLog(); 2931 loadForumLog();
2701 return; 2932 return;
2702 } 2933 }
2703 - 2934 +
2704 if (app === 'report') { 2935 if (app === 'report') {
2705 loadReportLog(); 2936 loadReportLog();
2706 return; 2937 return;
2707 } 2938 }
2708 - 2939 +
2709 fetch(`/api/output/${app}`) 2940 fetch(`/api/output/${app}`)
2710 .then(response => response.json()) 2941 .then(response => response.json())
2711 .then(data => { 2942 .then(data => {
  2943 + // 【FIX Bug #5】检查是否仍然是当前app,避免竞态条件
  2944 + // 如果用户已经切换到其他app,忽略这个响应
  2945 + if (currentApp !== app) {
  2946 + console.log(`忽略${app}的日志响应(当前app${currentApp})`);
  2947 + return;
  2948 + }
  2949 +
2712 if (data.success && data.output.length > 0) { 2950 if (data.success && data.output.length > 0) {
2713 const lastCount = lastLineCount[app] || 0; 2951 const lastCount = lastLineCount[app] || 0;
2714 const newLines = data.output.slice(lastCount); 2952 const newLines = data.output.slice(lastCount);
2715 - 2953 +
2716 if (newLines.length > 0) { 2954 if (newLines.length > 0) {
2717 newLines.forEach(line => { 2955 newLines.forEach(line => {
2718 appendConsoleTextLine(app, line); 2956 appendConsoleTextLine(app, line);
2719 }); 2957 });
2720 lastLineCount[app] = data.output.length; 2958 lastLineCount[app] = data.output.length;
  2959 +
  2960 + // 数据加载完成,更新加载提示为实际日志
  2961 + const renderer = logRenderers[app];
  2962 + if (renderer && renderer.lines.length > 0) {
  2963 + // 移除"正在加载"提示(如果存在)
  2964 + const firstLine = renderer.lines[0];
  2965 + if (firstLine && firstLine.text.includes('正在加载')) {
  2966 + renderer.lines.shift(); // 移除第一行
  2967 + renderer.lastRenderHash = null; // 强制重新渲染
  2968 + renderer.scheduleRender(true);
  2969 + }
  2970 + }
2721 } 2971 }
2722 } 2972 }
2723 }) 2973 })
2724 .catch(error => { 2974 .catch(error => {
2725 console.error('加载输出失败:', error); 2975 console.error('加载输出失败:', error);
  2976 + // 加载失败时也显示错误提示
  2977 + if (currentApp === app) {
  2978 + const renderer = logRenderers[app];
  2979 + if (renderer) {
  2980 + renderer.clear(`[错误] 加载${appNames[app] || app}日志失败`);
  2981 + renderer.render();
  2982 + }
  2983 + }
2726 }); 2984 });
2727 } 2985 }
2728 2986
@@ -3201,7 +3459,21 @@ @@ -3201,7 +3459,21 @@
3201 fetch('/api/forum/log') 3459 fetch('/api/forum/log')
3202 .then(response => response.json()) 3460 .then(response => response.json())
3203 .then(data => { 3461 .then(data => {
3204 - if (!data.success) return; 3462 + // 【FIX Bug #5】检查是否仍然在forum页面
  3463 + if (currentApp !== 'forum') {
  3464 + console.log('忽略forum日志响应(已切换到其他app)');
  3465 + return;
  3466 + }
  3467 +
  3468 + if (!data.success) {
  3469 + // 加载失败,显示错误
  3470 + const renderer = logRenderers['forum'];
  3471 + if (renderer) {
  3472 + renderer.clear('[错误] 加载Forum日志失败');
  3473 + renderer.render();
  3474 + }
  3475 + return;
  3476 + }
3205 3477
3206 const chatArea = document.getElementById('forumChatArea'); 3478 const chatArea = document.getElementById('forumChatArea');
3207 if (chatArea) { 3479 if (chatArea) {
@@ -3213,6 +3485,7 @@ @@ -3213,6 +3485,7 @@
3213 3485
3214 if (logLines.length > 0) { 3486 if (logLines.length > 0) {
3215 clearConsoleLayer('forum', '[系统] Forum Engine 日志输出'); 3487 clearConsoleLayer('forum', '[系统] Forum Engine 日志输出');
  3488 + logRenderers['forum'].render(); // 立即渲染清空提示
3216 logLines.forEach(line => appendConsoleTextLine('forum', line)); 3489 logLines.forEach(line => appendConsoleTextLine('forum', line));
3217 } else { 3490 } else {
3218 forumLogLineCount = 0; 3491 forumLogLineCount = 0;
@@ -3226,6 +3499,14 @@ @@ -3226,6 +3499,14 @@
3226 }) 3499 })
3227 .catch(error => { 3500 .catch(error => {
3228 console.error('加载论坛日志失败:', error); 3501 console.error('加载论坛日志失败:', error);
  3502 + // 【优化】显示错误提示
  3503 + if (currentApp === 'forum') {
  3504 + const renderer = logRenderers['forum'];
  3505 + if (renderer) {
  3506 + renderer.clear('[错误] 加载Forum日志失败: ' + error.message);
  3507 + renderer.render();
  3508 + }
  3509 + }
3229 }); 3510 });
3230 } 3511 }
3231 3512
@@ -3341,9 +3622,16 @@ @@ -3341,9 +3622,16 @@
3341 fetch('/api/report/log') 3622 fetch('/api/report/log')
3342 .then(response => response.json()) 3623 .then(response => response.json())
3343 .then(data => { 3624 .then(data => {
  3625 + // 【FIX Bug #5】检查是否仍然在report页面
  3626 + if (currentApp !== 'report') {
  3627 + console.log('忽略report日志响应(已切换到其他app)');
  3628 + return;
  3629 + }
  3630 +
3344 if (data.success) { 3631 if (data.success) {
3345 if (reportLogLineCount === 0) { 3632 if (reportLogLineCount === 0) {
3346 clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动'); 3633 clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动');
  3634 + logRenderers['report'].render(); // 立即渲染
3347 } 3635 }
3348 3636
3349 if (data.log_lines && data.log_lines.length > 0) { 3637 if (data.log_lines && data.log_lines.length > 0) {
@@ -3353,17 +3641,43 @@ @@ -3353,17 +3641,43 @@
3353 linesToProcess.forEach(line => { 3641 linesToProcess.forEach(line => {
3354 appendConsoleTextLine('report', line); 3642 appendConsoleTextLine('report', line);
3355 }); 3643 });
3356 - 3644 +
3357 // 重置计数器以确保后续消息能正确显示 3645 // 重置计数器以确保后续消息能正确显示
3358 reportLogLineCount = data.log_lines.length; 3646 reportLogLineCount = data.log_lines.length;
  3647 +
  3648 + // 移除"正在加载"提示(如果存在)
  3649 + const renderer = logRenderers['report'];
  3650 + if (renderer && renderer.lines.length > 0) {
  3651 + const firstLine = renderer.lines[0];
  3652 + if (firstLine && firstLine.text.includes('正在加载')) {
  3653 + renderer.lines.shift();
  3654 + renderer.lastRenderHash = null;
  3655 + renderer.scheduleRender(true);
  3656 + }
  3657 + }
3359 } else { 3658 } else {
3360 // 如果没有日志,重置计数器 3659 // 如果没有日志,重置计数器
3361 reportLogLineCount = 0; 3660 reportLogLineCount = 0;
3362 } 3661 }
  3662 + } else {
  3663 + // 【优化】加载失败显示错误
  3664 + const renderer = logRenderers['report'];
  3665 + if (renderer && currentApp === 'report') {
  3666 + renderer.clear('[错误] 加载Report日志失败');
  3667 + renderer.render();
  3668 + }
3363 } 3669 }
3364 }) 3670 })
3365 .catch(error => { 3671 .catch(error => {
3366 console.error('加载Report日志失败:', error); 3672 console.error('加载Report日志失败:', error);
  3673 + // 【优化】显示错误提示
  3674 + if (currentApp === 'report') {
  3675 + const renderer = logRenderers['report'];
  3676 + if (renderer) {
  3677 + renderer.clear('[错误] 加载Report日志失败: ' + error.message);
  3678 + renderer.render();
  3679 + }
  3680 + }
3367 }); 3681 });
3368 } 3682 }
3369 3683