merge: 剖面盒遍历优化(组节点剪枝 + 后序传播,大模型 ~2.1 倍提速)
This commit is contained in:
commit
da81672782
23
CHANGELOG.md
23
CHANGELOG.md
@ -1,5 +1,28 @@
|
|||||||
# NavisworksTransport 变更日志
|
# 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
|
## [0.18.2] - 2026-08-05
|
||||||
|
|
||||||
### ✨ 功能变更
|
### ✨ 功能变更
|
||||||
|
|||||||
@ -75,6 +75,19 @@ namespace NavisworksTransport.UnitTests.Integration
|
|||||||
status,
|
status,
|
||||||
"F1 批处理未在时限内完成。itemId=" + itemId +
|
"F1 批处理未在时限内完成。itemId=" + itemId +
|
||||||
(string.IsNullOrEmpty(errorMessage) ? "" : ",错误: " + errorMessage));
|
(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>
|
/// </summary>
|
||||||
public SectionBoxTraversalResult GetObjectsAndHiddenItems(Document document, BoundingBox3D sectionBox)
|
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 result = new SectionBoxTraversalResult();
|
||||||
var visibleSet = new HashSet<ModelItem>(); // 收集需要保留的节点(对象+祖先)
|
var visibleSet = new HashSet<ModelItem>(); // 收集需要保留的节点(对象+祖先)
|
||||||
@ -276,7 +284,7 @@ namespace NavisworksTransport.Core
|
|||||||
var rootItem = model.RootItem;
|
var rootItem = model.RootItem;
|
||||||
if (rootItem != null)
|
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>
|
/// <summary>
|
||||||
/// 递归遍历收集盒内对象和可见节点
|
/// 递归遍历收集盒内对象和可见节点。
|
||||||
|
/// 返回:本子树是否存在盒内对象(后序传播用——命中路径的祖先由父级逐层加入 visibleSet)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox,
|
private bool TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox,
|
||||||
List<ModelItem> objectsInBox, HashSet<ModelItem> visibleSet)
|
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();
|
var bbox = item.BoundingBox();
|
||||||
|
|
||||||
@ -315,27 +339,34 @@ namespace NavisworksTransport.Core
|
|||||||
double sizeZ = bbox.Max.Z - bbox.Min.Z;
|
double sizeZ = bbox.Max.Z - bbox.Min.Z;
|
||||||
bool hasVolume = sizeX > 0.0001 && sizeY > 0.0001 && sizeZ > 0.0001;
|
bool hasVolume = sizeX > 0.0001 && sizeY > 0.0001 && sizeZ > 0.0001;
|
||||||
|
|
||||||
// 有体积且与剖面盒相交 → 是目标对象
|
// 有体积且与剖面盒相交 → 是目标对象(只加入自己,祖先由父级后序上传)
|
||||||
if (hasVolume && BoundingBoxesIntersect(bbox, sectionBox))
|
if (hasVolume && BoundingBoxesIntersect(bbox, sectionBox))
|
||||||
{
|
{
|
||||||
objectsInBox.Add(item);
|
objectsInBox.Add(item);
|
||||||
|
visibleSet.Add(item);
|
||||||
// 将该对象及其所有祖先标记为可见
|
return true;
|
||||||
foreach (var ancestor in item.AncestorsAndSelf)
|
|
||||||
{
|
|
||||||
visibleSet.Add(ancestor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到几何体,停止向下遍历(原逻辑)
|
// 找到几何体,停止向下遍历(原逻辑)
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有几何体,继续遍历子节点
|
// 没有几何体,继续遍历子节点;后序传播:任一子命中 → 自己是祖先,加入 visibleSet
|
||||||
|
bool anyChildHit = false;
|
||||||
foreach (var child in item.Children)
|
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;
|
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) &&
|
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
|
||||||
string.Equals(request.Path, "/api/test/status", 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()
|
private object BuildStatusPayload()
|
||||||
{
|
{
|
||||||
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
|
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.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)));
|
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)
|
if (traversal.ObjectsInBox.Count == 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("指定包围盒内没有找到对象");
|
throw new InvalidOperationException("指定包围盒内没有找到对象");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var traversalResultPayload = new
|
||||||
|
{
|
||||||
|
traversalMs = sw.ElapsedMilliseconds,
|
||||||
|
prune
|
||||||
|
};
|
||||||
|
|
||||||
string exportedPath = exporter.ExportToNwd(document, traversal, filePath);
|
string exportedPath = exporter.ExportToNwd(document, traversal, filePath);
|
||||||
if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
|
if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
|
||||||
{
|
{
|
||||||
@ -781,7 +914,9 @@ namespace NavisworksTransport.Core.Services
|
|||||||
filePath = exportedPath,
|
filePath = exportedPath,
|
||||||
objectCount = traversal.ObjectsInBox.Count,
|
objectCount = traversal.ObjectsInBox.Count,
|
||||||
hiddenNodeCount = traversal.ItemsToHide.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
|
return new
|
||||||
{
|
{
|
||||||
|
isExecuting = manager.IsExecuting,
|
||||||
|
queueCount = manager.QueueCount,
|
||||||
items = items.Select(item => new
|
items = items.Select(item => new
|
||||||
{
|
{
|
||||||
itemId = item.Id,
|
itemId = item.Id,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user