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