From 539e4a9f2cc669e9c71a038c3e9e4a38240ba60c Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 6 Aug 2026 10:28:59 +0800 Subject: [PATCH 1/3] =?UTF-8?q?perf:=20=E5=89=96=E9=9D=A2=E7=9B=92?= =?UTF-8?q?=E9=81=8D=E5=8E=86=E7=BB=84=E8=8A=82=E7=82=B9=E8=81=9A=E5=90=88?= =?UTF-8?q?=E7=9B=92=E5=89=AA=E6=9E=9D=EF=BC=88=E5=A4=A7=E5=9E=8B=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=20~1.8=20=E5=80=8D=E6=8F=90=E9=80=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SectionBoxExporter: - TraverseAndCollect 对非几何节点(组节点)先查聚合包围盒, 与剖面盒不相交 → 整棵子树跳过(NW BoundingBox 为文档级缓存 O(1), 已用 Architecture.nwd 2 万节点 + 全厂模型 403MB 验证) - GetObjectsAndHiddenItems 增加 prune 参数(默认 true,false 供性能对比) 验证(全厂设备模型 403MB,100x100x40 测试盒): - 剪枝 72.9s vs 无剪枝 129.9s(~1.8 倍) - 盒内对象/隐藏节点完全一致(3521/80043,正确性无变化) - Architecture.nwd(2 万节点):273ms vs 580ms(~2.1 倍) - 集成测试 25/25 测试基础设施: - export-section-box 端点加 prune 参数 + traversalMs 计时 - bbox-profiling 端点加 enum=false 模式(根盒查询,避免大模型全枚举 10+ 分钟) - BatchQueueF1ProcessAutomationTests 末尾等待后台批处理完全退出 (修复 F1 后台任务残留导致后续 BatchQueue_AddItems isExecuting 断言失败) - batch-queue-status 端点加 isExecuting 字段 --- .../BatchQueueF1ProcessAutomationTests.cs | 13 ++ src/Core/SectionBoxExporter.cs | 31 +++- .../Services/TestAutomationHttpService.cs | 141 +++++++++++++++++- 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/UnitTests/Integration/BatchQueueF1ProcessAutomationTests.cs b/UnitTests/Integration/BatchQueueF1ProcessAutomationTests.cs index 2003231..815db28 100644 --- a/UnitTests/Integration/BatchQueueF1ProcessAutomationTests.cs +++ b/UnitTests/Integration/BatchQueueF1ProcessAutomationTests.cs @@ -75,6 +75,19 @@ namespace NavisworksTransport.UnitTests.Integration status, "F1 批处理未在时限内完成。itemId=" + itemId + (string.IsNullOrEmpty(errorMessage) ? "" : ",错误: " + errorMessage)); + + // 等待后台批处理完全退出(避免后台任务残留影响后续测试的 isExecuting 断言) + DateTime exitDeadline = DateTime.UtcNow.AddSeconds(90); + while (DateTime.UtcNow < exitDeadline) + { + JObject statusResp = await client.BatchQueueStatusAsync(1).ConfigureAwait(false); + bool isExecuting = (bool?)statusResp["data"]?["isExecuting"] ?? false; + if (!isExecuting) + { + break; + } + await Task.Delay(2000).ConfigureAwait(false); + } } } } diff --git a/src/Core/SectionBoxExporter.cs b/src/Core/SectionBoxExporter.cs index 2f8193e..a2e54ae 100644 --- a/src/Core/SectionBoxExporter.cs +++ b/src/Core/SectionBoxExporter.cs @@ -266,6 +266,14 @@ namespace NavisworksTransport.Core /// 然后使用原来的兄弟节点算法计算隐藏节点 /// public SectionBoxTraversalResult GetObjectsAndHiddenItems(Document document, BoundingBox3D sectionBox) + { + return GetObjectsAndHiddenItems(document, sectionBox, prune: true); + } + + /// + /// 遍历模型树收集盒内对象(prune=false 时禁用组节点聚合盒剪枝,用于性能对比)。 + /// + public SectionBoxTraversalResult GetObjectsAndHiddenItems(Document document, BoundingBox3D sectionBox, bool prune) { var result = new SectionBoxTraversalResult(); var visibleSet = new HashSet(); // 收集需要保留的节点(对象+祖先) @@ -276,7 +284,7 @@ namespace NavisworksTransport.Core var rootItem = model.RootItem; if (rootItem != null) { - TraverseAndCollect(model.RootItem, sectionBox, result.ObjectsInBox, visibleSet); + TraverseAndCollect(model.RootItem, sectionBox, result.ObjectsInBox, visibleSet, prune); } } @@ -300,12 +308,27 @@ namespace NavisworksTransport.Core /// 递归遍历收集盒内对象和可见节点 /// private void TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox, - List objectsInBox, HashSet visibleSet) + List objectsInBox, HashSet visibleSet, bool prune) { if (item == null || item.IsHidden) return; + bool hasGeometry = item.HasGeometry; + + // 子树剪枝:非几何节点(组节点)先查聚合包围盒(NW 内部缓存,O(1))。 + // 组节点盒与剖面盒不相交 → 其整棵子树内不可能存在盒内对象 → 跳过全部后代。 + // 收益:大型模型下远处楼层/区域一次调用剪掉整棵子树,省下所有后代的 + // Children/IsHidden/HasGeometry API 调用。 + if (prune && !hasGeometry) + { + var groupBox = item.BoundingBox(); + if (!BoundingBoxesIntersect(groupBox, sectionBox)) + { + return; + } + } + // 如果有几何体,检查是否在盒内 - if (item.HasGeometry) + if (hasGeometry) { var bbox = item.BoundingBox(); @@ -334,7 +357,7 @@ namespace NavisworksTransport.Core // 没有几何体,继续遍历子节点 foreach (var child in item.Children) { - TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet); + TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune); } } diff --git a/src/Core/Services/TestAutomationHttpService.cs b/src/Core/Services/TestAutomationHttpService.cs index 642d140..31d5210 100644 --- a/src/Core/Services/TestAutomationHttpService.cs +++ b/src/Core/Services/TestAutomationHttpService.cs @@ -231,6 +231,14 @@ namespace NavisworksTransport.Core.Services return; } + if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) && + string.Equals(request.Path, "/api/test/bbox-profiling", StringComparison.OrdinalIgnoreCase)) + { + object payload = InvokeOnUiThread(() => BuildBBoxProfilingPayload(request.Query)); + await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false); + return; + } + if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) && string.Equals(request.Path, "/api/test/status", StringComparison.OrdinalIgnoreCase)) { @@ -447,6 +455,121 @@ namespace NavisworksTransport.Core.Services } } + /// + /// 【实验端点】NW 组节点/几何节点 BoundingBox 缓存行为分析。 + /// 验证:组节点聚合盒是否为 O(1) 缓存读取(用于剖面盒遍历子树剪枝)。 + /// + private object BuildBBoxProfilingPayload(Dictionary query) + { + Document doc = Autodesk.Navisworks.Api.Application.ActiveDocument; + if (doc == null || doc.IsClear) + { + return new { error = "无活动文档" }; + } + + bool enumOnly = ParseBooleanQuery(query, "enum", true); + var sw = new System.Diagnostics.Stopwatch(); + + // 根盒(不枚举,O(1))——大模型下全量枚举需数分钟,根盒/剪枝验证不需要枚举 + sw.Restart(); + var rootBox = doc.Models[0].RootItem.BoundingBox(); + long rootFirstMs = sw.ElapsedMilliseconds; + + sw.Restart(); + var rootBox2 = doc.Models[0].RootItem.BoundingBox(); + long rootSecondMs = sw.ElapsedMilliseconds; + + if (!enumOnly) + { + return new + { + enumSkipped = true, + rootFirstMs, + rootSecondMs, + rootBox = $"({rootBox.Min.X:F1},{rootBox.Min.Y:F1},{rootBox.Min.Z:F1})~({rootBox.Max.X:F1},{rootBox.Max.Y:F1},{rootBox.Max.Z:F1})" + }; + } + + // 全量枚举计时(RootItemDescendantsAndSelf 本身耗时) + sw.Restart(); + var allItems = doc.Models.SelectMany(m => m.RootItem?.DescendantsAndSelf ?? Enumerable.Empty()).ToList(); + long enumMs = sw.ElapsedMilliseconds; + + int totalCount = allItems.Count; + var groupNodes = allItems.Where(i => !i.HasGeometry && i.Children != null && i.Children.Count() > 0).Take(200).ToList(); + var geometryNodes = allItems.Where(i => i.HasGeometry).Take(200).ToList(); + + // 实验A(冷):不预先调用任何 BoundingBox,直接批量测组节点盒 + sw.Restart(); + long groupColdTotalMs = 0; + int groupColdCount = 0; + foreach (var g in groupNodes) + { + sw.Restart(); + var b = g.BoundingBox(); + groupColdTotalMs += sw.ElapsedMilliseconds; + groupColdCount++; + } + + // 实验A2(冷):几何节点盒 + sw.Restart(); + long geomColdTotalMs = 0; + int geomColdCount = 0; + foreach (var g in geometryNodes) + { + sw.Restart(); + var b = g.BoundingBox(); + geomColdTotalMs += sw.ElapsedMilliseconds; + geomColdCount++; + } + + // 实验B(热):根盒已在开头测过(缓存),直接批量测组节点盒 + sw.Restart(); + long groupHotTotalMs = 0; + int groupHotCount = 0; + foreach (var g in groupNodes) + { + sw.Restart(); + var b = g.BoundingBox(); + groupHotTotalMs += sw.ElapsedMilliseconds; + groupHotCount++; + } + + // 重复调用同一组节点(缓存特征:第二次起应接近 0) + ModelItem sampleGroup = groupNodes.FirstOrDefault(); + long repeatFirstMs = 0; + long repeatSecondMs = 0; + if (sampleGroup != null) + { + sw.Restart(); + var b1 = sampleGroup.BoundingBox(); + repeatFirstMs = sw.ElapsedMilliseconds; + + sw.Restart(); + var b2 = sampleGroup.BoundingBox(); + repeatSecondMs = sw.ElapsedMilliseconds; + } + + return new + { + enumMs, + totalCount, + groupSampleCount = groupColdCount, + geometrySampleCount = geomColdCount, + groupColdAvgMs = groupColdCount > 0 ? groupColdTotalMs / (double)groupColdCount : 0, + geomColdAvgMs = geomColdCount > 0 ? geomColdTotalMs / (double)geomColdCount : 0, + groupColdTotalMs, + geomColdTotalMs, + rootFirstMs, + rootSecondMs, + groupHotAvgMs = groupHotCount > 0 ? groupHotTotalMs / (double)groupHotCount : 0, + groupHotTotalMs, + repeatFirstMs, + repeatSecondMs, + rootBox = $"({rootBox.Min.X:F1},{rootBox.Min.Y:F1},{rootBox.Min.Z:F1})~({rootBox.Max.X:F1},{rootBox.Max.Y:F1},{rootBox.Max.Z:F1})" + }; + } + private object BuildStatusPayload() { Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument; @@ -761,12 +884,22 @@ namespace NavisworksTransport.Core.Services new Point3D(Math.Min(x1, x2), Math.Min(y1, y2), Math.Min(z1, z2)), new Point3D(Math.Max(x1, x2), Math.Max(y1, y2), Math.Max(z1, z2))); - var traversal = exporter.GetObjectsAndHiddenItems(document, bounds); + bool prune = ParseBooleanQuery(query, "prune", true); + var sw = new System.Diagnostics.Stopwatch(); + sw.Start(); + var traversal = exporter.GetObjectsAndHiddenItems(document, bounds, prune); + sw.Stop(); if (traversal.ObjectsInBox.Count == 0) { throw new InvalidOperationException("指定包围盒内没有找到对象"); } + var traversalResultPayload = new + { + traversalMs = sw.ElapsedMilliseconds, + prune + }; + string exportedPath = exporter.ExportToNwd(document, traversal, filePath); if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath)) { @@ -781,7 +914,9 @@ namespace NavisworksTransport.Core.Services filePath = exportedPath, objectCount = traversal.ObjectsInBox.Count, hiddenNodeCount = traversal.ItemsToHide.Count, - fileSize = new FileInfo(exportedPath).Length + fileSize = new FileInfo(exportedPath).Length, + traversalMs = traversalResultPayload.traversalMs, + prune = traversalResultPayload.prune }; } @@ -968,6 +1103,8 @@ namespace NavisworksTransport.Core.Services return new { + isExecuting = manager.IsExecuting, + queueCount = manager.QueueCount, items = items.Select(item => new { itemId = item.Id, From 636defc9593de372e627fbf44066daac94570ec9 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 6 Aug 2026 10:37:48 +0800 Subject: [PATCH 2/3] =?UTF-8?q?perf:=20AncestorsAndSelf=20=E5=90=8E?= =?UTF-8?q?=E5=BA=8F=E4=BC=A0=E6=92=AD=EF=BC=88=E5=91=BD=E4=B8=AD=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E7=A5=96=E5=85=88=E9=80=90=E5=B1=82=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=8C=E6=B6=88=E9=99=A4=20K=C3=97D=20=E6=AC=A1=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TraverseAndCollect 改为返回 bool(子树是否有盒内对象): - 命中时只 visibleSet.Add(item) 自己 - 组节点 children 循环后:任一子命中 → 自己加入 visibleSet(post-order 上传) - 共享祖先只被加入一次,AncestorsAndSelf 枚举完全消除 验证: - 盒内对象/隐藏节点与优化前完全一致(228/4928)✓ - Architecture.nwd:~273ms → ~265ms(命中数小时收益小;大模型命中数大时更明显) - 集成测试 25/25 --- src/Core/SectionBoxExporter.cs | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Core/SectionBoxExporter.cs b/src/Core/SectionBoxExporter.cs index a2e54ae..5eefeb8 100644 --- a/src/Core/SectionBoxExporter.cs +++ b/src/Core/SectionBoxExporter.cs @@ -305,12 +305,13 @@ namespace NavisworksTransport.Core } /// - /// 递归遍历收集盒内对象和可见节点 + /// 递归遍历收集盒内对象和可见节点。 + /// 返回:本子树是否存在盒内对象(后序传播用——命中路径的祖先由父级逐层加入 visibleSet)。 /// - private void TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox, + private bool TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox, List objectsInBox, HashSet visibleSet, bool prune) { - if (item == null || item.IsHidden) return; + if (item == null || item.IsHidden) return false; bool hasGeometry = item.HasGeometry; @@ -323,7 +324,7 @@ namespace NavisworksTransport.Core var groupBox = item.BoundingBox(); if (!BoundingBoxesIntersect(groupBox, sectionBox)) { - return; + return false; } } @@ -338,27 +339,34 @@ namespace NavisworksTransport.Core double sizeZ = bbox.Max.Z - bbox.Min.Z; bool hasVolume = sizeX > 0.0001 && sizeY > 0.0001 && sizeZ > 0.0001; - // 有体积且与剖面盒相交 → 是目标对象 + // 有体积且与剖面盒相交 → 是目标对象(只加入自己,祖先由父级后序上传) if (hasVolume && BoundingBoxesIntersect(bbox, sectionBox)) { objectsInBox.Add(item); - - // 将该对象及其所有祖先标记为可见 - foreach (var ancestor in item.AncestorsAndSelf) - { - visibleSet.Add(ancestor); - } + visibleSet.Add(item); + return true; } // 找到几何体,停止向下遍历(原逻辑) - return; + return false; } - // 没有几何体,继续遍历子节点 + // 没有几何体,继续遍历子节点;后序传播:任一子命中 → 自己是祖先,加入 visibleSet + bool anyChildHit = false; foreach (var child in item.Children) { - TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune); + if (TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune)) + { + anyChildHit = true; + } } + + if (anyChildHit) + { + visibleSet.Add(item); + } + + return anyChildHit; } /// From 8e16d28d45b705ab6e3a57bf486dffb33eb7fe81 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 6 Aug 2026 10:43:12 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=E7=89=88=E6=9C=AC=200.18.3=20?= =?UTF-8?q?=E2=80=94=20=E5=89=96=E9=9D=A2=E7=9B=92=E9=81=8D=E5=8E=86?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=88=E5=89=AA=E6=9E=9D=20~1.8x=20+=20?= =?UTF-8?q?=E5=90=8E=E5=BA=8F=E4=BC=A0=E6=92=AD=20~15%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 23 +++++++++++++++++++++++ VERSION.md | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb75205..3563c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # NavisworksTransport 变更日志 +## [0.18.3] - 2026-08-06 + +### ⚡ 性能优化 + +- **剖面盒遍历组节点聚合盒剪枝(大型模型 ~1.8 倍提速)**: + - `TraverseAndCollect` 对非几何节点(组节点)先查聚合包围盒,与剖面盒不相交 → **整棵子树跳过**(不再深入任何后代) + - 前提验证:NW 的 `BoundingBox()` 为文档级缓存(O(1) 读取,Architecture.nwd 2 万节点 + 全厂设备模型 403MB 实测全 0ms)——真正成本是每节点 Children/IsHidden/HasGeometry API 调用,剪枝省掉的正是这些 + - 实测(全厂设备模型 403MB,100×100×40 测试盒,3521 命中对象):**129.9s → 72.9s**;细长路径剖面盒(相交子树少)收益比例更高 +- **AncestorsAndSelf 后序传播(消除 K×D 次枚举,再降 ~15%)**: + - 命中对象只加入自己,祖先由父级 post-order 逐层上传(`TraverseAndCollect` 返回子树命中标志)——共享祖先只被加入一次,消除 K 个命中对象 × 树深 D 的 NW API 枚举 + - 实测(同测试环境):**72.9s → 62.0s**;两项叠加 **129.9s → 62.0s(~2.1 倍)** +- 正确性验证:盒内对象/隐藏节点与优化前完全一致(3521/80043;Architecture.nwd 228/4928) + +### 🧪 测试基础设施 + +- `export-section-box` 测试端点增加 `prune` 参数 + `traversalMs` 计时(性能对比用) +- `bbox-profiling` 端点增加 `enum=false` 模式(根盒查询,避免大模型全量枚举 10+ 分钟) +- **F1 测试后台残留修复**:`BatchQueueF1ProcessAutomationTests` 末尾等待批处理完全退出(`batch-queue-status` 新增 `isExecuting` 字段),修复 F1 跑完后全量测试 `BatchQueue_AddItems` 偶发失败 + +### 验证 + +- 集成测试 25/25 通过 + ## [0.18.2] - 2026-08-05 ### ✨ 功能变更 diff --git a/VERSION.md b/VERSION.md index eda4ce5..aad11e3 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,3 +1,3 @@ # 版本号 -0.18.2 +0.18.3