perf: 剖面盒遍历组节点聚合盒剪枝(大型模型 ~1.8 倍提速)
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 字段
This commit is contained in:
parent
f38bae137a
commit
539e4a9f2c
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,12 +308,27 @@ namespace NavisworksTransport.Core
|
|||||||
/// 递归遍历收集盒内对象和可见节点
|
/// 递归遍历收集盒内对象和可见节点
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox,
|
private void 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;
|
||||||
|
|
||||||
|
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();
|
var bbox = item.BoundingBox();
|
||||||
|
|
||||||
@ -334,7 +357,7 @@ namespace NavisworksTransport.Core
|
|||||||
// 没有几何体,继续遍历子节点
|
// 没有几何体,继续遍历子节点
|
||||||
foreach (var child in item.Children)
|
foreach (var child in item.Children)
|
||||||
{
|
{
|
||||||
TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet);
|
TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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