merge: 剖面盒遍历优化(组节点剪枝 + 后序传播,大模型 ~2.1 倍提速)
This commit is contained in:
commit
da81672782
23
CHANGELOG.md
23
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
|
||||
|
||||
### ✨ 功能变更
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
# 版本号
|
||||
|
||||
0.18.2
|
||||
0.18.3
|
||||
|
||||
@ -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,29 +339,36 @@ 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>
|
||||
/// 【使用原来的兄弟节点算法】计算需要隐藏的节点
|
||||
/// </summary>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user