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,