merge: 剖面盒遍历优化(组节点剪枝 + 后序传播,大模型 ~2.1 倍提速)

This commit is contained in:
tian 2026-08-06 10:43:12 +08:00
commit da81672782
5 changed files with 223 additions and 19 deletions

View File

@ -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 调用,剪枝省掉的正是这些
- 实测(全厂设备模型 403MB100×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/80043Architecture.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
### ✨ 功能变更

View File

@ -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);
}
}
}
}

View File

@ -1,3 +1,3 @@
# 版本号
0.18.2
0.18.3

View File

@ -266,6 +266,14 @@ namespace NavisworksTransport.Core
/// 然后使用原来的兄弟节点算法计算隐藏节点
/// </summary>
public SectionBoxTraversalResult GetObjectsAndHiddenItems(Document document, BoundingBox3D sectionBox)
{
return GetObjectsAndHiddenItems(document, sectionBox, prune: true);
}
/// <summary>
/// 遍历模型树收集盒内对象prune=false 时禁用组节点聚合盒剪枝,用于性能对比)。
/// </summary>
public SectionBoxTraversalResult GetObjectsAndHiddenItems(Document document, BoundingBox3D sectionBox, bool prune)
{
var result = new SectionBoxTraversalResult();
var visibleSet = new HashSet<ModelItem>(); // 收集需要保留的节点(对象+祖先)
@ -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);
}
}
@ -297,15 +305,31 @@ namespace NavisworksTransport.Core
}
/// <summary>
/// 递归遍历收集盒内对象和可见节点
/// 递归遍历收集盒内对象和可见节点。
/// 返回:本子树是否存在盒内对象(后序传播用——命中路径的祖先由父级逐层加入 visibleSet
/// </summary>
private void TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox,
List<ModelItem> objectsInBox, HashSet<ModelItem> visibleSet)
private bool TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox,
List<ModelItem> objectsInBox, HashSet<ModelItem> visibleSet, bool prune)
{
if (item == null || item.IsHidden) return;
if (item == null || item.IsHidden) return false;
bool hasGeometry = item.HasGeometry;
// 子树剪枝非几何节点组节点先查聚合包围盒NW 内部缓存O(1))。
// 组节点盒与剖面盒不相交 → 其整棵子树内不可能存在盒内对象 → 跳过全部后代。
// 收益:大型模型下远处楼层/区域一次调用剪掉整棵子树,省下所有后代的
// Children/IsHidden/HasGeometry API 调用。
if (prune && !hasGeometry)
{
var groupBox = item.BoundingBox();
if (!BoundingBoxesIntersect(groupBox, sectionBox))
{
return false;
}
}
// 如果有几何体,检查是否在盒内
if (item.HasGeometry)
if (hasGeometry)
{
var bbox = item.BoundingBox();
@ -315,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);
if (TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune))
{
anyChildHit = true;
}
}
if (anyChildHit)
{
visibleSet.Add(item);
}
return anyChildHit;
}
/// <summary>

View File

@ -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
}
}
/// <summary>
/// 【实验端点】NW 组节点/几何节点 BoundingBox 缓存行为分析。
/// 验证:组节点聚合盒是否为 O(1) 缓存读取(用于剖面盒遍历子树剪枝)。
/// </summary>
private object BuildBBoxProfilingPayload(Dictionary<string, string> 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<ModelItem>()).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,