Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
Expand Down
4 changes: 4 additions & 0 deletions src/Gui/FeatureTimeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ QIcon timelineObjectIcon(
std::optional<bool> 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);
Expand Down Expand Up @@ -1391,6 +1393,8 @@ void FeatureTimeline::rebuild()
addCurrentStateMarker();
}

ZoneScopedN("Gui.FeatureTimeline.rebuild.items");

const bool isCurrent = static_cast<int>(index) == lastActiveVisible;
const bool afterPosition = !controller->isOperationActive(object);
auto* owner = operationOwner(object, projection);
Expand Down
3 changes: 3 additions & 0 deletions src/Gui/ModelTreeBrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <App/OriginGroupExtension.h>
#include <App/PropertyLinks.h>
#include <App/PropertyStandard.h>
#include <Base/Profiler.h>


using namespace Gui;
Expand Down Expand Up @@ -160,6 +161,8 @@ App::DocumentObject* exactVibeScriptProgramFor(

ModelTreeBrowserProjection::ModelTreeBrowserProjection(App::Document* document)
{
ZoneScopedN("Gui.ModelTreeBrowserProjection.build");

if (!document) {
return;
}
Expand Down
14 changes: 14 additions & 0 deletions src/Gui/Tree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -5524,6 +5526,8 @@ DocumentObjectItem* DocumentItem::createBrowserObjectItem(
bool browserDefaultHidden
)
{
ZoneScopedN("Gui.Tree.createBrowserObjectItem");

if (!object || !parent) {
return nullptr;
}
Expand Down Expand Up @@ -5811,6 +5815,8 @@ void DocumentItem::rebuildModelBrowser()
QTreeWidgetItem* parent,
DocumentObjectItem* logicalParent
) -> DocumentObjectItem* {
ZoneScopedN("Gui.Tree.rebuildModelBrowser.items");

if (!entryAvailable(entry)) {
return nullptr;
}
Expand Down Expand Up @@ -7836,6 +7842,8 @@ void TreeWidget::updateChildren(
bool force
)
{
ZoneScopedN("Gui.Tree.updateChildren");

bool childrenChanged = false;
std::vector<App::DocumentObject*> children;
bool removeChildrenFromRoot = true;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
Expand Down
30 changes: 30 additions & 0 deletions src/Mod/VibeCAD/vibecad_tests/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)}"
Loading