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
55 changes: 50 additions & 5 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,22 @@ Status values are `DONE`, `IN PROGRESS`, `NOT STARTED`, `BLOCKED`, and
| P00 | Establish cooperative mutation lease and block conflicting document mutations | DONE | PR #168; native GUI tests 138/138 |
| 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 recorder and opt-in Qt watchdog added in `4daa8d06`; native instrumentation will reuse the existing Tracy integration |
| 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 |
| 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 | NOT STARTED | Native trace identifies per-solid costs |
| 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 |
| 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 |
| 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 | NOT STARTED | B05 and B07 final-tail 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 |
| 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 |
| P17 | Audit and eliminate synchronous filesystem/network/process waits in UI callbacks | NOT STARTED | Static audit plus runtime trace shows zero occurrences |
| P18 | Validate and optimize application startup and document restore | NOT STARTED | B01/B02 cold/warm evidence |
| P17 | Audit and eliminate synchronous filesystem/network/process waits in UI callbacks | IN PROGRESS | Native trace confirms synchronous save/restore exceed the GUI budget; complete static/runtime audit and fixes remain |
| P18 | Validate and optimize application startup and document restore | IN PROGRESS | Synthetic restore trace captured; packaged 2,000-object B01/B02 cold/warm evidence remains |
| P19 | Validate and optimize Analyze/FEM/CFD workflows | NOT STARTED | B11-B13 evidence |
| P20 | Validate and optimize Drawing/TechDraw workflows | NOT STARTED | B14-B16 evidence |
| P21 | Validate and optimize Assembly edit, solve, motion, and collision workflows | NOT STARTED | B08-B10 evidence |
Expand Down Expand Up @@ -397,6 +397,51 @@ the commit under test.
standard multi-selection Hide, and folder Space-toggle produced zero
synchronous traversals and exactly one stable delayed traversal.

### 2026-09-04 — native and cooperative hot-path tracing

- Base: `99044673`.
- Commit under test: `d116a0c96cf33ec2610170b24dbf5e02e9d40d48`.
- Host: Windows 11 Pro for Workstations 10.0.26200; Intel Xeon Gold 5120,
14 cores/28 logical processors; 127.7 GiB RAM.
- Reuse boundary: native spans use the existing `Base/Profiler.h` Tracy
integration. Python GUI-apply spans use the existing bounded performance
recorder through one optional scheduler hook. Disabled tracing performs no
timer or file I/O and adds one null check per cooperative slice.
- Red tests: cooperative execution initially had no span-factory connection;
VibeScript publication initially supplied no stable trace identity. Both
focused tests failed before their respective implementation changes.
- Focused Python command:
`python -m pytest test_cooperative_document_execution.py test_performance.py
test_vibescript_publication_progress.py test_native_background.py
test_native_analyze_solver_execution.py test_native_drawing_view.py
test_native_registry.py -q`. Result: 86 passed.
- Exact committed branch-tip Windows package command from
`package/rattler-build`: `pixi install -e default --frozen`. Result: success
in 17 minutes; 8,384 files and 489.45 MiB packaged as
`vibecad-26.3.1RC6-h3c70cbc_1.conda`. The installed runtime imports both
`jsonschema` and YAML through the existing provider-dependency path.
- Packaged GUI trace probe: four real document-thread apply slices ran on the
GUI native thread while the Qt heartbeat delivered 45 samples. Result:
`VIBECAD_PERFORMANCE_TRACE_GUI_OK slices=4 heartbeat_samples=45`, exit code
zero. The earlier trace-enabled build of the same code path delivered 48
heartbeat samples.
- Tracy build: full Windows Release build and install completed with the
profiler enabled. A clean configure without `TRACY_STATIC` on the command
line produced `TRACY_STATIC:BOOL=OFF` and a shared `TracyClient.dll`, proving
the parent CMake default now survives Tracy's older `option()` policy.
- Native trace workload: create and display 24 independent boxes, bulk-toggle
visibility, commit and abort transactions, undo/redo, save, close, restore,
recompute, and save again. The workload completed and the 231 KiB capture
exported all named native zones.
- Observed maxima in this instrumentation fixture: object creation 8.31 ms;
tessellation 11.14 ms; Coin construction 0.16 ms; full visual update
11.58 ms; Tree status update 14.26 ms; Timeline rebuild 0.55 ms; save
58.62 ms; restore 127.23 ms.
- Interpretation: this is proof that the instrumentation records real work,
not B02/B07 completion. It identifies synchronous save/restore and the Tree
path as budget violations or near-violations, and it keeps P03, P06, P13,
P17, and P18 open until representative-scale before/after evidence exists.

## PR discipline

This program may require multiple small PRs, but they all track this one plan.
Expand Down
3 changes: 1 addition & 2 deletions src/3rdParty/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ if (BUILD_TRACY_FRAME_PROFILER)
message(FATAL_ERROR "The Tracy git directory is not available. Please clone it manually." )
endif()

set(TRACY_STATIC OFF)
set(TRACY_STATIC OFF CACHE BOOL "Whether to build Tracy as a static library")
add_subdirectory(tracy)
endif()

18 changes: 17 additions & 1 deletion src/App/Document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ bool Document::checkOnCycle()

bool Document::undo(const int id)
{
ZoneScopedN("App.Document.undo");

if (isCooperativeMutationActive()) {
FC_WARN("Cannot undo while a cooperative document mutation is active");
return false;
Expand Down Expand Up @@ -240,6 +242,8 @@ bool Document::undo(const int id)

bool Document::redo(const int id)
{
ZoneScopedN("App.Document.redo");

if (isCooperativeMutationActive()) {
FC_WARN("Cannot redo while a cooperative document mutation is active");
return false;
Expand Down Expand Up @@ -659,6 +663,8 @@ void Document::commitTransaction() // NOLINT

bool Document::_commitTransaction(const bool notify)
{
ZoneScopedN("App.Document.commitTransaction");

if (isPerformingTransaction()) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot commit transaction while transacting");
Expand Down Expand Up @@ -730,6 +736,8 @@ void Document::abortTransaction() const

void Document::_abortTransaction()
{
ZoneScopedN("App.Document.abortTransaction");

if (isPerformingTransaction() || d->committing) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot abort transaction while transacting");
Expand Down Expand Up @@ -2232,6 +2240,8 @@ bool Document::save()

bool Document::saveToFile(const char* filename) const
{
ZoneScopedN("App.Document.saveToFile");

signalStartSave(*this, filename);

auto hGrp = GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document");
Expand Down Expand Up @@ -2414,6 +2424,8 @@ bool Document::isAnyRestoring()
// Open the document
void Document::restore(const char* filename, bool delaySignal, const std::vector<std::string>& objNames)
{
ZoneScopedN("App.Document.restore");

clearUndos();
d->activeObject = nullptr;

Expand Down Expand Up @@ -3145,7 +3157,7 @@ void Document::renameObjectIdentifiers(

int Document::recompute(const std::vector<DocumentObject*>& objs, bool force, bool* hasError, int options)
{
ZoneScoped;
ZoneScopedN("App.Document.recompute");

if (isCooperativeMutationActive()) {
FC_WARN("Cannot recompute while a cooperative document mutation is active");
Expand Down Expand Up @@ -3877,6 +3889,8 @@ void Document::_addObject(
const char* viewType
)
{
ZoneScopedN("App.Document.addObject");

struct AddObjectCriticalScope
{
explicit AddObjectCriticalScope(DocumentP& state)
Expand Down Expand Up @@ -4102,6 +4116,8 @@ void Document::removeObject(const char* sName)
}
void Document::_removeObject(DocumentObject* pcObject, RemoveObjectOptions options)
{
ZoneScopedN("App.Document.removeObject");

if (!options.testFlag(RemoveObjectOption::MayRemoveWhileRecomputing)
&& testStatus(Document::Recomputing)) {
FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing");
Expand Down
5 changes: 5 additions & 0 deletions src/Gui/FeatureTimeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include <App/SuppressibleExtension.h>
#include <Base/Console.h>
#include <Base/Exception.h>
#include <Base/Profiler.h>

#include "ActiveObjectList.h"
#include "Action.h"
Expand Down Expand Up @@ -1203,6 +1204,8 @@ void FeatureTimeline::setObservedDocument(Gui::Document* document)

void FeatureTimeline::scheduleRefresh()
{
ZoneScopedN("Gui.FeatureTimeline.scheduleRefresh");

if (refreshTimer && !refreshTimer->isActive()) {
refreshTimer->start();
}
Expand All @@ -1226,6 +1229,8 @@ bool FeatureTimeline::canChangeHistory() const

void FeatureTimeline::rebuild()
{
ZoneScopedN("Gui.FeatureTimeline.rebuild");

App::Document* document = activeAppDocument();
if (Gui::Document::projectionRefreshBlocked(document)) {
// Undo, redo, and transaction rollback recreate objects in dependency
Expand Down
9 changes: 9 additions & 0 deletions src/Gui/Tree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include <string_view>

#include <Base/Console.h>
#include <Base/Profiler.h>
#include <Base/Reader.h>
#include <Base/Sequencer.h>
#include <Base/Tools.h>
Expand Down Expand Up @@ -4370,6 +4371,8 @@ struct UpdateDisabler

void TreeWidget::onUpdateStatus()
{
ZoneScopedN("Gui.Tree.onUpdateStatus");

if (this->state() == DraggingState || App::GetApplication().isRestoring()) {
_updateStatus();
return;
Expand Down Expand Up @@ -5640,6 +5643,8 @@ void DocumentItem::setLegacyTreeVisible(bool visible)

void DocumentItem::updateBrowserFolderStatus()
{
ZoneScopedN("Gui.Tree.updateBrowserFolderStatus");

if (!modelBrowserActive) {
return;
}
Expand Down Expand Up @@ -5677,6 +5682,8 @@ void DocumentItem::updateBrowserFolderStatus()

void DocumentItem::rebuildModelBrowser()
{
ZoneScopedN("Gui.Tree.rebuildModelBrowser");

auto* tree = getTree();
auto* appDocument = document()->getDocument();
if (!tree || !appDocument) {
Expand Down Expand Up @@ -7101,6 +7108,8 @@ void DocumentItem::rebuildModelBrowser()

void DocumentItem::refreshModelBrowser(bool force)
{
ZoneScopedN("Gui.Tree.refreshModelBrowser");

if (!TreeParams::getOrganizeModelByType()) {
if (modelBrowserActive) {
clearModelBrowser();
Expand Down
18 changes: 14 additions & 4 deletions src/Mod/Part/Gui/ViewProviderExt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
#include <App/Application.h>
#include <App/Document.h>
#include <Base/Console.h>
#include <Base/Profiler.h>
#include <Base/Parameter.h>
#include <Base/Sequencer.h>
#include <Base/TimeInfo.h>
Expand Down Expand Up @@ -1220,6 +1221,8 @@ void ViewProviderPartExt::setupCoinGeometry(
bool normalsFromUV
)
{
ZoneNamedN(setupCoinGeometryZone, "Part.ViewProvider.setupCoinGeometry", true);

if (Part::Tools::isShapeEmpty(shape)) {
coords->point.setNum(0);
norm->vector.setNum(0);
Expand Down Expand Up @@ -1264,14 +1267,19 @@ void ViewProviderPartExt::setupCoinGeometry(
meshParams.AllowQualityDecrease = Standard_True;

// Clear triangulation and PCurves from geometry which can slow down the process
{
ZoneNamedN(tessellationZone, "Part.ViewProvider.tessellate", true);
#if OCC_VERSION_HEX < 0x070600
BRepTools::Clean(shape);
BRepTools::Clean(shape);
#else
BRepTools::Clean(shape, Standard_True);
BRepTools::Clean(shape, Standard_True);
#endif

BRepMesh_IncrementalMesh(shape, meshParams);

BRepMesh_IncrementalMesh(shape, meshParams);
}

ZoneNamedN(coinGeometryZone, "Part.ViewProvider.buildCoinGeometry", true);

// We must reset the location here because the transformation data
// are set in the placement property
TopLoc_Location aLoc;
Expand Down Expand Up @@ -1634,6 +1642,8 @@ void ViewProviderPartExt::setupCoinGeometry(

void ViewProviderPartExt::updateVisual()
{
ZoneScopedN("Part.ViewProvider.updateVisual");

auto* object = getObject();
auto* document = object ? object->getDocument() : nullptr;
if (isRestoring()
Expand Down
35 changes: 34 additions & 1 deletion src/Mod/VibeCAD/VibeCADCooperativeExecution.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@
from typing import Any

DEFAULT_DOCUMENT_SLICE_BUDGET_SECONDS = 0.05
_document_thread_span_factory: Callable[..., Any] | None = None


class CooperativeExecutionCancelled(RuntimeError):
"""Cancellation injected between two atomic document-thread slices."""


def _set_document_thread_span_factory(factory: Callable[..., Any] | None) -> None:
"""Connect opt-in tracing without importing it on the normal hot path."""

if factory is not None and not callable(factory):
raise TypeError("A document-thread span factory must be callable or None.")
global _document_thread_span_factory
_document_thread_span_factory = factory


def _advance(steps: Iterator[Any]) -> tuple[bool, Any]:
try:
return False, next(steps)
Expand Down Expand Up @@ -52,6 +62,7 @@ def run_document_thread_steps(
progress_callback: Callable[[dict[str, Any]], None] | None = None,
clock: Callable[[], float] = time.monotonic,
slice_budget_seconds: float = DEFAULT_DOCUMENT_SLICE_BUDGET_SECONDS,
trace_attributes: Mapping[str, Any] | None = None,
) -> Any:
"""Drive one resumable document job from its background owner.

Expand All @@ -69,12 +80,34 @@ def run_document_thread_steps(
budget = max(0.001, float(slice_budget_seconds))
invoke = dispatch or (lambda operation: operation())

slice_index = 0

def advance_or_cancel() -> tuple[bool, Any]:
nonlocal slice_index
# Check again inside the dispatched callable. Cancellation can arrive
# after a worker queues a slice but before Qt starts executing it.
if cancellation_check is not None and cancellation_check():
_cancel(steps)
return _advance(steps)
span_factory = _document_thread_span_factory
if span_factory is None:
return _advance(steps)
attributes = dict(trace_attributes or {})
attributes["slice_index"] = slice_index
slice_index += 1
with span_factory(
"document.apply_slice",
category="ui",
gui_thread=True,
attributes=attributes,
):
completed, value = _advance(steps)
if isinstance(value, Mapping):
for key in ("phase", "completed", "total", "output_type"):
if key in value and isinstance(value[key], (str, int)):
attributes[key] = value[key]
if completed:
attributes["completed_operation"] = True
return completed, value

try:
while True:
Expand Down
5 changes: 5 additions & 0 deletions src/Mod/VibeCAD/VibeCADNativeBackground.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,11 @@ def report_commit_progress(event: Mapping[str, Any]) -> None:
dispatch=dispatch_to_document_thread,
cancellation_check=job.cancellation.is_set,
progress_callback=report_commit_progress,
trace_attributes={
"operation_id": job.job_id,
"document_uid": job.document_uid,
"capability": job.capability_name,
},
)
except CooperativeExecutionCancelled as exc:
raise NativeBackgroundCancelled() from exc
Expand Down
7 changes: 7 additions & 0 deletions src/Mod/VibeCAD/VibeCADPerformanceGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def install_event_loop_watchdog(
if watchdog is None and not recorder.enabled:
return None

from VibeCADCooperativeExecution import _set_document_thread_span_factory

_set_document_thread_span_factory(recorder.span if recorder.enabled else None)

if parent is None:
import FreeCADGui as Gui

Expand Down Expand Up @@ -82,3 +86,6 @@ def _reset_event_loop_watchdog_for_tests() -> None:
stop = getattr(timer, "stop", None)
if callable(stop):
stop()
from VibeCADCooperativeExecution import _set_document_thread_span_factory

_set_document_thread_span_factory(None)
6 changes: 6 additions & 0 deletions src/Mod/VibeCAD/VibeCADVibeScriptDomainRuntime.py
Original file line number Diff line number Diff line change
Expand Up @@ -17468,6 +17468,12 @@ def publish_cooperatively(
dispatch=document_thread_dispatch,
cancellation_check=cancellation_check,
progress_callback=progress_callback,
trace_attributes={
"operation_id": str(prepared.get("attempt_id") or ""),
"document_uid": str(prepared.get("document_uid") or ""),
"capability": "vibescript.publish",
"domain": self.pack.domain,
},
)

def inspect(
Expand Down
Loading
Loading