From 1637990314bc122b870a15c6cc86c8d57715fdee Mon Sep 17 00:00:00 2001 From: RobitOverlord <193709524+10-X-eng@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:32:36 -0500 Subject: [PATCH] Measure representative publication projection costs --- PERFORMANCE.md | 53 +++++++++++++++++-- src/Gui/FeatureTimeline.cpp | 4 ++ src/Gui/ModelTreeBrowser.cpp | 3 ++ src/Gui/Tree.cpp | 14 +++++ .../VibeCAD/vibecad_tests/test_performance.py | 30 +++++++++++ 5 files changed, 100 insertions(+), 4 deletions(-) diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 21003566..15cf5685 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -223,6 +223,51 @@ set for representative large documents. | B26 | Cancel/close/undo stress | every long-running class | integrity, no deadlock, bounded response | | B27 | Multi-document load | three active documents/jobs | fairness, memory, document isolation | +## Recorded representative baselines + +### B07: 307-member VibeScript Assembly publication + +Recorded 2026-09-04 on Windows 11 Pro for Workstations build 26200, using an +Intel Xeon Gold 5120 (14 cores/28 logical processors) and 127.7 GiB RAM. The +instrumented binaries were built from `da2e1b39` plus the trace-only changes in +this branch through the normal Rattler CMake recipe. The deterministic fixture +publishes 307 component links, one Assembly, and one solver-diagnostics output. + +Command: + +```powershell +build/run_vibescript_publication_307_native_trace.ps1 +``` + +The local runner and trace artifact remain ignored; no machine paths or fixture +outputs are committed. + +| Measurement | Baseline | +|---|---:| +| Correct publication | 307 members; 322 document objects; passed | +| Wall / operation lifecycle | 21.360 s / 19.516 s | +| Worker / publication | 7.422 s / 11.063 s | +| GUI apply units | 317 | +| Longest cooperative apply unit | 93.518 ms | +| GUI heartbeat p99 / maximum | 65.112 ms / 1,863.733 ms | +| Heartbeat failures over 100 ms | 3 | +| Final Tree stable refresh | 963.229 ms | +| Model-browser rebuild inside Tree refresh | 473.543 ms | +| Browser item creation | 463.308 ms across 321 items | +| Tree status-icon generation | 912.865 ms across 642 generated icons | +| Final Feature Timeline rebuild | 842.690 ms | +| Timeline item construction | 834.236 ms across 312 items | +| Timeline icon transformation | 393.617 ms across 312 icons | +| Semantic browser projection | 4.7 ms maximum | + +This trace proves that the 1.864-second final heartbeat gap is not semantic +classification or BREP tessellation. It is the sequential stable Tree and +Feature Timeline refresh. The largest repeated costs are generating the same +object-status icon separately for the legacy and semantic Tree items, and +raster-transforming equivalent Timeline icons once per operation. Optimize +those measured repetitions before changing projection architecture. Remaining +Qt item construction must then be applied in bounded event-loop batches. + ## Work plan and ledger Status values are `DONE`, `IN PROGRESS`, `NOT STARTED`, `BLOCKED`, and @@ -234,16 +279,16 @@ Status values are `DONE`, `IN PROGRESS`, `NOT STARTED`, `BLOCKED`, and | P01 | Yield VibeScript publication between output/member apply steps | DONE | PR #168; Python suite 4,380 passed, 9 skipped; large Assembly became interactively usable | | P02 | Add publication phase/item progress | DONE | PR #168; direct VibeScript progress callback tests | | P03 | Instrument cross-language spans and GUI watchdog | IN PROGRESS | Bounded Python trace/watchdog in `4daa8d06`; real GUI-thread apply spans and native Tracy capture proven in `d116a0c9`; remaining required surfaces are still open | -| P04 | Capture cold/warm baseline for B02, B06, B07, B09, and B24 | NOT STARTED | Trace and benchmark summaries | +| P04 | Capture cold/warm baseline for B02, B06, B07, B09, and B24 | IN PROGRESS | Representative B07 Windows baseline recorded above; cold/warm B02, B06, B09, and B24 remain | | P05 | Remove Assembly drag/bulk-visibility refresh storm | DONE | Packaged red tests proved duplicate deferred traversal and synchronous per-selection traversal across 153 visibility changes; packaged green test proved direct, standard multi-selection, and folder-toggle paths each converge in exactly one delayed refresh | | P06 | Measure BREP assignment, tessellation, and Coin construction independently | IN PROGRESS | Synthetic native trace separates tessellation, Coin construction, and full visual update; representative B07 fixture remains | | P07 | Pre-tessellate eligible solids in isolated workers and cache render meshes | NOT STARTED | Same visual/correct BREP output; B07 comparison | -| P08 | Drive ordinary per-object GUI apply cost toward <= 1 ms | NOT STARTED | B07 per-item distribution and longest span | +| P08 | Drive ordinary per-object GUI apply cost toward <= 1 ms | IN PROGRESS | B07: 317 units, 93.518 ms maximum; per-member publication and projection-item work remain | | P09 | Add adaptive batching for proven-cheap apply units | NOT STARTED | Higher throughput without heartbeat regression | -| P10 | Bound publication commit/rollback/final-stabilization phases | NOT STARTED | No unnamed GUI span > 16 ms in B07/B26 | +| P10 | Bound publication commit/rollback/final-stabilization phases | IN PROGRESS | B07 final Tree + Timeline tail isolated at 1.806 s; optimization and B26 remain | | P11 | Coalesce Tree, Timeline, cache, and observer notifications | NOT STARTED | One logical refresh per operation | | P12 | Eliminate full-object stable refresh when exact changed identities exist | NOT STARTED | B05/B07 traversal and allocation evidence | -| P13 | Measure and optimize Tree projection and Feature Timeline rebuild | IN PROGRESS | Synthetic native trace measures Tree and Timeline paths; B05/B07 scale and optimization remain | +| P13 | Measure and optimize Tree projection and Feature Timeline rebuild | IN PROGRESS | Representative B07 native trace isolates Tree status-icon generation (912.865 ms) and Timeline icon transformation (393.617 ms); optimization remains | | P14 | Implement persistent startup worker runtime and bounded queues | NOT STARTED | Startup, reuse, fairness, shutdown, and crash tests | | P15 | Implement per-document actor ownership and revision-safe result application | NOT STARTED | B26/B27 stress evidence | | P16 | Standardize human/AI progress, cancellation, and diagnostics contract | REVALIDATE | VibeScript direct progress exists; audit every other long operation | diff --git a/src/Gui/FeatureTimeline.cpp b/src/Gui/FeatureTimeline.cpp index fadae627..cf9aff12 100644 --- a/src/Gui/FeatureTimeline.cpp +++ b/src/Gui/FeatureTimeline.cpp @@ -111,6 +111,8 @@ QIcon timelineObjectIcon( std::optional presentationVisible ) { + ZoneScopedN("Gui.FeatureTimeline.icon"); + QPixmap pixmap = base.pixmap(QSize(22, 22), disabled ? QIcon::Disabled : QIcon::Normal); if (disabled || (presentationVisible.has_value() && !*presentationVisible)) { pixmap = desaturatePixmap(pixmap); @@ -1391,6 +1393,8 @@ void FeatureTimeline::rebuild() addCurrentStateMarker(); } + ZoneScopedN("Gui.FeatureTimeline.rebuild.items"); + const bool isCurrent = static_cast(index) == lastActiveVisible; const bool afterPosition = !controller->isOperationActive(object); auto* owner = operationOwner(object, projection); diff --git a/src/Gui/ModelTreeBrowser.cpp b/src/Gui/ModelTreeBrowser.cpp index 700d2743..8d07f21e 100644 --- a/src/Gui/ModelTreeBrowser.cpp +++ b/src/Gui/ModelTreeBrowser.cpp @@ -21,6 +21,7 @@ #include #include #include +#include using namespace Gui; @@ -160,6 +161,8 @@ App::DocumentObject* exactVibeScriptProgramFor( ModelTreeBrowserProjection::ModelTreeBrowserProjection(App::Document* document) { + ZoneScopedN("Gui.ModelTreeBrowserProjection.build"); + if (!document) { return; } diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 82c44275..a96c7ee8 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -5451,6 +5451,8 @@ bool DocumentItem::createNewItem( DocumentObjectDataPtr data ) { + ZoneScopedN("Gui.Tree.createNewItem"); + if (!obj.getObject() || !obj.getObject()->isAttachedToDocument() || obj.getObject()->testStatus(App::PartialObject)) { return false; @@ -5524,6 +5526,8 @@ DocumentObjectItem* DocumentItem::createBrowserObjectItem( bool browserDefaultHidden ) { + ZoneScopedN("Gui.Tree.createBrowserObjectItem"); + if (!object || !parent) { return nullptr; } @@ -5811,6 +5815,8 @@ void DocumentItem::rebuildModelBrowser() QTreeWidgetItem* parent, DocumentObjectItem* logicalParent ) -> DocumentObjectItem* { + ZoneScopedN("Gui.Tree.rebuildModelBrowser.items"); + if (!entryAvailable(entry)) { return nullptr; } @@ -7836,6 +7842,8 @@ void TreeWidget::updateChildren( bool force ) { + ZoneScopedN("Gui.Tree.updateChildren"); + bool childrenChanged = false; std::vector children; bool removeChildrenFromRoot = true; @@ -9270,6 +9278,8 @@ void DocumentObjectItem::setIconOverlays(int currentStatus, QPixmap& overlays) c void DocumentObjectItem::generateIcon(int currentStatus, QIcon::Mode mode, QIcon& icon) { + ZoneScopedN("Gui.Tree.generateIcon"); + QPixmap px; if (currentStatus & Status::Error) { static QPixmap pxError; @@ -9323,6 +9333,8 @@ void DocumentObjectItem::generateIcon(int currentStatus, QIcon::Mode mode, QIcon QIcon DocumentObjectItem::getVisibilityIcon(int currentStatus, QIcon& original_icon) { + ZoneScopedN("Gui.Tree.getVisibilityIcon"); + static QPixmap pxVisible, pxInvisible; if (pxVisible.isNull()) { pxVisible = BitmapFactory().pixmap("TreeItemVisible"); @@ -9363,6 +9375,8 @@ QIcon DocumentObjectItem::getVisibilityIcon(int currentStatus, QIcon& original_i void DocumentObjectItem::testStatus(bool resetStatus, QIcon& icon1, QIcon& icon2) { + ZoneScopedN("Gui.Tree.testStatus"); + // guard against calling this during destruction when tree widget may be nullptr if (!treeWidget()) { return; diff --git a/src/Mod/VibeCAD/vibecad_tests/test_performance.py b/src/Mod/VibeCAD/vibecad_tests/test_performance.py index 642b048f..f6c23e67 100644 --- a/src/Mod/VibeCAD/vibecad_tests/test_performance.py +++ b/src/Mod/VibeCAD/vibecad_tests/test_performance.py @@ -3,12 +3,16 @@ from __future__ import annotations import json +from pathlib import Path import VibeCADCooperativeExecution as cooperative from VibeCADPerformance import EventLoopWatchdog, PerformanceRecorder import VibeCADPerformanceGui +REPOSITORY_ROOT = Path(__file__).resolve().parents[4] + + class _Clock: def __init__(self, values: list[int]): self._values = iter(values) @@ -293,3 +297,29 @@ def test_gui_watchdog_connects_opt_in_document_slice_tracing(monkeypatch): assert span_factory.__func__ is recorder.span.__func__ VibeCADPerformanceGui._reset_event_loop_watchdog_for_tests() assert cooperative._document_thread_span_factory is None + + +def test_native_projection_trace_separates_measured_hot_paths(): + sources = { + "src/Gui/ModelTreeBrowser.cpp": { + 'ZoneScopedN("Gui.ModelTreeBrowserProjection.build")', + }, + "src/Gui/Tree.cpp": { + 'ZoneScopedN("Gui.Tree.createNewItem")', + 'ZoneScopedN("Gui.Tree.updateChildren")', + 'ZoneScopedN("Gui.Tree.createBrowserObjectItem")', + 'ZoneScopedN("Gui.Tree.generateIcon")', + 'ZoneScopedN("Gui.Tree.getVisibilityIcon")', + 'ZoneScopedN("Gui.Tree.rebuildModelBrowser.items")', + 'ZoneScopedN("Gui.Tree.testStatus")', + }, + "src/Gui/FeatureTimeline.cpp": { + 'ZoneScopedN("Gui.FeatureTimeline.icon")', + 'ZoneScopedN("Gui.FeatureTimeline.rebuild.items")', + }, + } + + for relative_path, expected_spans in sources.items(): + source = (REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8") + missing = {span for span in expected_spans if span not in source} + assert not missing, f"{relative_path} lacks trace spans: {sorted(missing)}"