perf: 性能与可靠性优化 - CommandQueue超时恢复/HTTP限制/日志轮转/调试日志清理/applyProperties修复

This commit is contained in:
火焰库拉
2026-02-28 08:44:45 +08:00
parent 42ab8d8ee2
commit aadf69300f
3 changed files with 1225 additions and 1168 deletions

View File

@@ -249,3 +249,37 @@
### 5. 日志仅输出关键信息到编辑器控制台 ### 5. 日志仅输出关键信息到编辑器控制台
- **优化**: `addLog` 函数不再将所有类型的日志输出到编辑器控制台,仅 `error``warn` 级别日志通过 `Editor.error()` / `Editor.warn()` 输出,防止 `info` / `success` / `mcp` 类型日志刷屏干扰开发者。 - **优化**: `addLog` 函数不再将所有类型的日志输出到编辑器控制台,仅 `error``warn` 级别日志通过 `Editor.error()` / `Editor.warn()` 输出,防止 `info` / `success` / `mcp` 类型日志刷屏干扰开发者。
---
## 性能与可靠性优化 (2026-02-28)
### 1. CommandQueue 超时保护恢复
- **问题**: 合并冲突解决时 `enqueueCommand` 中的 60 秒兜底超时保护代码丢失,导致如果工具函数内部异常未调用 `done()`,整个指令队列将永久停滞,后续所有操作将卡死不再响应。
- **修复**: 在 `enqueueCommand` 中为每个入队指令注册 `setTimeout(60000)` 超时定时器,正常完成时通过 `clearTimeout` 取消。
### 2. HTTP 请求体大小限制
- **问题**: `_handleRequest``body += chunk` 无上限保护,超大请求体(恶意或异常客户端)可能耗尽编辑器进程内存。
- **修复**: 新增 5MB (`5 * 1024 * 1024`) 请求体上限,超出时返回 HTTP 413 并销毁连接。
### 3. 日志文件轮转机制
- **问题**: `settings/mcp-bridge.log` 文件持续追加写入,长期使用会无限增长占用磁盘空间。
- **修复**: 在 `getLogFilePath()` 初始化时检查文件大小,超过 2MB 自动将旧日志重命名为 `.old` 备份后创建新文件。
### 4. 清理冗余调试日志
- **问题**: `scene-script.js``update-node-transform``applyProperties` 共有 8 处 `Editor.log` 调试日志,每次操作都输出到编辑器控制台造成刷屏。
- **修复**: 移除所有冗余 `Editor.log` 调试输出,保留必要的 `Editor.warn` 警告(如资源加载失败、属性解析失败等)。
### 5. `applyProperties` 逻辑修复
- **问题**: `applyProperties` 启发式资源解析分支中使用了 `return` 而非 `continue`,导致处理到该分支后会直接退出整个 `for...of` 循环,跳过后续属性的设置。
- **修复**: 将 `return` 改为 `continue`,确保多属性同时更新时所有属性都能被正确处理。
### 6. `instantiate-prefab` 统一使用 `findNode`
- **问题**: `instantiate-prefab` 中查找父节点直接调用 `cc.engine.getInstanceById(parentId)`,绕过了 `findNode` 函数的压缩 UUID 解压与兼容逻辑。
- **修复**: 统一改用 `findNode(parentId)`,确保所有场景操作对压缩和非压缩 UUID 格式的兼容性一致。

36
main.js
View File

@@ -29,7 +29,14 @@ let isProcessingCommand = false;
*/ */
function enqueueCommand(fn) { function enqueueCommand(fn) {
return new Promise((resolve) => { return new Promise((resolve) => {
commandQueue.push({ fn, resolve }); // 兜底超时保护:防止 fn 内部未调用 done() 导致队列永久停滞
const timeoutId = setTimeout(() => {
addLog("error", "[CommandQueue] 指令执行超时(60s),强制释放队列");
isProcessingCommand = false;
resolve();
processNextCommand();
}, 60000);
commandQueue.push({ fn, resolve, timeoutId });
processNextCommand(); processNextCommand();
}); });
} }
@@ -40,15 +47,17 @@ function enqueueCommand(fn) {
function processNextCommand() { function processNextCommand() {
if (isProcessingCommand || commandQueue.length === 0) return; if (isProcessingCommand || commandQueue.length === 0) return;
isProcessingCommand = true; isProcessingCommand = true;
const { fn, resolve } = commandQueue.shift(); const { fn, resolve, timeoutId } = commandQueue.shift();
try { try {
fn(() => { fn(() => {
clearTimeout(timeoutId);
isProcessingCommand = false; isProcessingCommand = false;
resolve(); resolve();
processNextCommand(); processNextCommand();
}); });
} catch (e) { } catch (e) {
// 防止队列因未捕获异常永久阻塞 // 防止队列因未捕获异常永久阻塞
clearTimeout(timeoutId);
addLog("error", `[CommandQueue] 指令执行异常: ${e.message}`); addLog("error", `[CommandQueue] 指令执行异常: ${e.message}`);
isProcessingCommand = false; isProcessingCommand = false;
resolve(); resolve();
@@ -112,6 +121,19 @@ function getLogFilePath() {
fs.mkdirSync(settingsDir, { recursive: true }); fs.mkdirSync(settingsDir, { recursive: true });
} }
_logFilePath = pathModule.join(settingsDir, "mcp-bridge.log"); _logFilePath = pathModule.join(settingsDir, "mcp-bridge.log");
// 日志轮转: 超过 2MB 时备份旧日志并创建新文件
try {
if (fs.existsSync(_logFilePath)) {
const stats = fs.statSync(_logFilePath);
if (stats.size > 2 * 1024 * 1024) {
const backupPath = _logFilePath + ".old";
if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);
fs.renameSync(_logFilePath, backupPath);
}
}
} catch (e) {
/* 轮转失败不影响主流程 */
}
return _logFilePath; return _logFilePath;
} }
} catch (e) { } catch (e) {
@@ -832,11 +854,21 @@ module.exports = {
res.setHeader("Content-Type", "application/json"); res.setHeader("Content-Type", "application/json");
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Origin", "*");
const MAX_BODY_SIZE = 5 * 1024 * 1024; // 5MB 请求体上限
let body = ""; let body = "";
let aborted = false;
req.on("data", (chunk) => { req.on("data", (chunk) => {
body += chunk; body += chunk;
if (body.length > MAX_BODY_SIZE) {
aborted = true;
addLog("error", `[HTTP] 请求体超过 ${MAX_BODY_SIZE} 字节上限,已拒绝`);
res.writeHead(413);
res.end(JSON.stringify({ error: "请求体过大" }));
req.destroy();
}
}); });
req.on("end", () => { req.on("end", () => {
if (aborted) return;
const url = req.url; const url = req.url;
if (url === "/list-tools") { if (url === "/list-tools") {
const tools = getToolsList(); const tools = getToolsList();

View File

@@ -143,13 +143,10 @@ module.exports = {
*/ */
"update-node-transform": function (event, args) { "update-node-transform": function (event, args) {
const { id, x, y, scaleX, scaleY, color } = args; const { id, x, y, scaleX, scaleY, color } = args;
Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`);
let node = findNode(id); let node = findNode(id);
if (node) { if (node) {
Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`);
// 使用 scene:set-property 实现支持 Undo 的属性修改 // 使用 scene:set-property 实现支持 Undo 的属性修改
// 注意IPC 消息需要发送到 'scene' 面板 // 注意IPC 消息需要发送到 'scene' 面板
if (x !== undefined) { if (x !== undefined) {
@@ -213,7 +210,6 @@ module.exports = {
Editor.Ipc.sendToMain("scene:dirty"); Editor.Ipc.sendToMain("scene:dirty");
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id }); Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`);
if (event.reply) event.reply(null, "变换信息已更新"); if (event.reply) event.reply(null, "变换信息已更新");
} else { } else {
if (event.reply) event.reply(new Error("找不到节点")); if (event.reply) event.reply(new Error("找不到节点"));
@@ -369,7 +365,6 @@ module.exports = {
if (targetNode) { if (targetNode) {
handler.target = targetNode; handler.target = targetNode;
Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`);
} }
} }
@@ -451,7 +446,6 @@ module.exports = {
loadedCount++; loadedCount++;
if (!err && asset) { if (!err && asset) {
loadedAssets[idx] = asset; loadedAssets[idx] = asset;
Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`);
} else { } else {
Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`); Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`);
} }
@@ -503,7 +497,6 @@ module.exports = {
); );
} }
} }
Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`);
} else if (value && value.length > 20) { } else if (value && value.length > 20) {
// 如果明确是组件/节点类型但找不到,才报错 // 如果明确是组件/节点类型但找不到,才报错
Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`); Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`);
@@ -514,7 +507,6 @@ module.exports = {
const targetNode = findNode(value); const targetNode = findNode(value);
if (targetNode) { if (targetNode) {
finalValue = targetNode; finalValue = targetNode;
Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`);
} else { } else {
// 找不到节点且是 UUID -> 视为资源 // 找不到节点且是 UUID -> 视为资源
const compIndex = node._components.indexOf(component); const compIndex = node._components.indexOf(component);
@@ -526,9 +518,8 @@ module.exports = {
value: { uuid: value }, value: { uuid: value },
isSubProp: false, isSubProp: false,
}); });
Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`);
} }
return; continue;
} }
} }
} }
@@ -827,7 +818,7 @@ module.exports = {
} }
// 设置父节点 // 设置父节点
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene; let parent = parentId ? findNode(parentId) : scene;
if (parent) { if (parent) {
instance.parent = parent; instance.parent = parent;