paper-burner/js/chatbot/utils/katex-cache.js

284 lines
6.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Paper Burner - KaTeX 公式缓存系统
* Phase 4.2+: 解决公式渲染阻塞主线程问题(实测 4.6s 阻塞)
*
* 功能:
* - 缓存已渲染的公式,避免重复渲染
* - LRU 策略,限制缓存大小
* - 自动清理过期缓存
* - 性能监控集成
*
* 预期收益:
* - 重复公式渲染时间减少 99%(从 50ms 降至 0.5ms
* - 充满公式的聊天打开时间从 4.6s 降至 0.5s 以下
*/
(function() {
'use strict';
/**
* LRU 缓存实现
*/
class LRUCache {
constructor(maxSize = 1000) {
this.maxSize = maxSize;
this.cache = new Map();
this.stats = {
hits: 0,
misses: 0,
evictions: 0,
totalRenderTime: 0,
totalCacheTime: 0
};
}
/**
* 获取缓存项
*/
get(key) {
const startTime = performance.now();
if (!this.cache.has(key)) {
this.stats.misses++;
return null;
}
// LRU: 移到末尾(最近使用)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
this.stats.hits++;
this.stats.totalCacheTime += performance.now() - startTime;
return value;
}
/**
* 设置缓存项
*/
set(key, value) {
// 如果已存在,先删除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 如果超过最大容量,删除最老的项
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
this.stats.evictions++;
}
this.cache.set(key, value);
}
/**
* 清空缓存
*/
clear() {
this.cache.clear();
this.stats.hits = 0;
this.stats.misses = 0;
this.stats.evictions = 0;
this.stats.totalRenderTime = 0;
this.stats.totalCacheTime = 0;
}
/**
* 获取统计信息
*/
getStats() {
const total = this.stats.hits + this.stats.misses;
const hitRate = total > 0 ? (this.stats.hits / total * 100) : 0;
return {
size: this.cache.size,
maxSize: this.maxSize,
hits: this.stats.hits,
misses: this.stats.misses,
evictions: this.stats.evictions,
hitRate: hitRate.toFixed(1) + '%',
avgRenderTime: this.stats.misses > 0
? (this.stats.totalRenderTime / this.stats.misses).toFixed(2) + 'ms'
: '0ms',
avgCacheTime: this.stats.hits > 0
? (this.stats.totalCacheTime / this.stats.hits).toFixed(3) + 'ms'
: '0ms'
};
}
}
/**
* KaTeX 缓存管理器
*/
class KaTeXCache {
constructor(options = {}) {
this.maxSize = options.maxSize || 1000;
this.enableMonitoring = options.enableMonitoring !== false;
this.cache = new LRUCache(this.maxSize);
// KaTeX 渲染选项的默认值
this.defaultOptions = {
strict: 'ignore',
throwOnError: false,
trust: false,
macros: {},
maxSize: 50,
maxExpand: 100
};
}
/**
* 生成缓存键
* 包含公式内容和渲染选项
*/
generateKey(tex, options) {
const displayMode = options.displayMode ? '1' : '0';
const output = options.output || 'html';
// 只包含影响渲染结果的选项
return `${displayMode}:${output}:${tex}`;
}
/**
* 渲染公式(带缓存)
*/
render(tex, options = {}) {
// 合并选项
const renderOptions = { ...this.defaultOptions, ...options };
// 生成缓存键
const cacheKey = this.generateKey(tex, renderOptions);
// 检查缓存
const cached = this.cache.get(cacheKey);
if (cached !== null) {
return cached;
}
// 缓存未命中,渲染公式
const startTime = performance.now();
let result;
try {
result = katex.renderToString(tex, renderOptions);
} catch (error) {
// 渲染失败也缓存,避免重复尝试
result = null;
}
const duration = performance.now() - startTime;
this.cache.stats.totalRenderTime += duration;
// 性能监控
if (this.enableMonitoring && window.PerfMonitor) {
window.PerfMonitor.recordRender(duration, 'katex_render');
}
// 存入缓存
this.cache.set(cacheKey, result);
return result;
}
/**
* 批量预渲染公式
* 用于聊天历史加载时提前渲染常见公式
*/
async preRender(formulas, options = {}) {
const results = [];
for (const tex of formulas) {
const result = this.render(tex, options);
results.push({ tex, result });
// 避免阻塞主线程太久
if (results.length % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
return results;
}
/**
* 获取缓存统计
*/
getStats() {
return this.cache.getStats();
}
/**
* 清空缓存
*/
clear() {
this.cache.clear();
}
/**
* 导出缓存(用于持久化)
*/
export() {
const entries = Array.from(this.cache.cache.entries());
return {
version: 1,
timestamp: Date.now(),
entries: entries,
stats: this.cache.stats
};
}
/**
* 导入缓存(从持久化恢复)
*/
import(data) {
if (!data || data.version !== 1) {
console.warn('[KaTeXCache] Invalid cache data');
return false;
}
try {
this.cache.cache.clear();
data.entries.forEach(([key, value]) => {
this.cache.cache.set(key, value);
});
return true;
} catch (error) {
console.error('[KaTeXCache] Failed to import cache:', error);
return false;
}
}
}
// 创建全局实例
window.KaTeXCache = KaTeXCache;
// 自动初始化默认实例
if (!window.katexCache) {
window.katexCache = new KaTeXCache({
maxSize: 1000,
enableMonitoring: true
});
console.log('[KaTeXCache] Formula cache initialized with maxSize: 1000');
}
// 提供全局便捷方法
window.renderKatexCached = function(tex, options) {
return window.katexCache.render(tex, options);
};
// 在控制台提供统计查看方法
window.getKatexCacheStats = function() {
const stats = window.katexCache.getStats();
console.log('KaTeX 缓存统计:');
console.table(stats);
return stats;
};
})();
console.log('[KaTeXCache] KaTeX formula cache system loaded');
console.log(' Usage: renderKatexCached(tex, options)');
console.log(' Stats: getKatexCacheStats()');