diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 589100a0c..21003566c 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -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 | @@ -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. diff --git a/src/3rdParty/CMakeLists.txt b/src/3rdParty/CMakeLists.txt index c1120057b..d78aaf0fd 100644 --- a/src/3rdParty/CMakeLists.txt +++ b/src/3rdParty/CMakeLists.txt @@ -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() - diff --git a/src/App/Document.cpp b/src/App/Document.cpp index d4c38e79d..67cba017b 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -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; @@ -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; @@ -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"); @@ -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"); @@ -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"); @@ -2414,6 +2424,8 @@ bool Document::isAnyRestoring() // Open the document void Document::restore(const char* filename, bool delaySignal, const std::vector& objNames) { + ZoneScopedN("App.Document.restore"); + clearUndos(); d->activeObject = nullptr; @@ -3145,7 +3157,7 @@ void Document::renameObjectIdentifiers( int Document::recompute(const std::vector& 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"); @@ -3877,6 +3889,8 @@ void Document::_addObject( const char* viewType ) { + ZoneScopedN("App.Document.addObject"); + struct AddObjectCriticalScope { explicit AddObjectCriticalScope(DocumentP& state) @@ -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"); diff --git a/src/Gui/FeatureTimeline.cpp b/src/Gui/FeatureTimeline.cpp index 9767def37..fadae6274 100644 --- a/src/Gui/FeatureTimeline.cpp +++ b/src/Gui/FeatureTimeline.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include "ActiveObjectList.h" #include "Action.h" @@ -1203,6 +1204,8 @@ void FeatureTimeline::setObservedDocument(Gui::Document* document) void FeatureTimeline::scheduleRefresh() { + ZoneScopedN("Gui.FeatureTimeline.scheduleRefresh"); + if (refreshTimer && !refreshTimer->isActive()) { refreshTimer->start(); } @@ -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 diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index 04596fba0..82c442750 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -44,6 +44,7 @@ #include #include +#include #include #include #include @@ -4370,6 +4371,8 @@ struct UpdateDisabler void TreeWidget::onUpdateStatus() { + ZoneScopedN("Gui.Tree.onUpdateStatus"); + if (this->state() == DraggingState || App::GetApplication().isRestoring()) { _updateStatus(); return; @@ -5640,6 +5643,8 @@ void DocumentItem::setLegacyTreeVisible(bool visible) void DocumentItem::updateBrowserFolderStatus() { + ZoneScopedN("Gui.Tree.updateBrowserFolderStatus"); + if (!modelBrowserActive) { return; } @@ -5677,6 +5682,8 @@ void DocumentItem::updateBrowserFolderStatus() void DocumentItem::rebuildModelBrowser() { + ZoneScopedN("Gui.Tree.rebuildModelBrowser"); + auto* tree = getTree(); auto* appDocument = document()->getDocument(); if (!tree || !appDocument) { @@ -7101,6 +7108,8 @@ void DocumentItem::rebuildModelBrowser() void DocumentItem::refreshModelBrowser(bool force) { + ZoneScopedN("Gui.Tree.refreshModelBrowser"); + if (!TreeParams::getOrganizeModelByType()) { if (modelBrowserActive) { clearModelBrowser(); diff --git a/src/Mod/Part/Gui/ViewProviderExt.cpp b/src/Mod/Part/Gui/ViewProviderExt.cpp index 54387a4a1..f3dc7541f 100644 --- a/src/Mod/Part/Gui/ViewProviderExt.cpp +++ b/src/Mod/Part/Gui/ViewProviderExt.cpp @@ -79,6 +79,7 @@ #include #include #include +#include #include #include #include @@ -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); @@ -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; @@ -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() diff --git a/src/Mod/VibeCAD/VibeCADCooperativeExecution.py b/src/Mod/VibeCAD/VibeCADCooperativeExecution.py index 1216a40bb..b97fcb7b7 100644 --- a/src/Mod/VibeCAD/VibeCADCooperativeExecution.py +++ b/src/Mod/VibeCAD/VibeCADCooperativeExecution.py @@ -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) @@ -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. @@ -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: diff --git a/src/Mod/VibeCAD/VibeCADNativeBackground.py b/src/Mod/VibeCAD/VibeCADNativeBackground.py index b8ba6f1ce..8c77265ae 100644 --- a/src/Mod/VibeCAD/VibeCADNativeBackground.py +++ b/src/Mod/VibeCAD/VibeCADNativeBackground.py @@ -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 diff --git a/src/Mod/VibeCAD/VibeCADPerformanceGui.py b/src/Mod/VibeCAD/VibeCADPerformanceGui.py index f0c068428..9192c4a44 100644 --- a/src/Mod/VibeCAD/VibeCADPerformanceGui.py +++ b/src/Mod/VibeCAD/VibeCADPerformanceGui.py @@ -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 @@ -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) diff --git a/src/Mod/VibeCAD/VibeCADVibeScriptDomainRuntime.py b/src/Mod/VibeCAD/VibeCADVibeScriptDomainRuntime.py index eda61d24e..933c2ac63 100644 --- a/src/Mod/VibeCAD/VibeCADVibeScriptDomainRuntime.py +++ b/src/Mod/VibeCAD/VibeCADVibeScriptDomainRuntime.py @@ -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( diff --git a/src/Mod/VibeCAD/vibecad_tests/test_cooperative_document_execution.py b/src/Mod/VibeCAD/vibecad_tests/test_cooperative_document_execution.py index e13d39ffd..4e23a50b7 100644 --- a/src/Mod/VibeCAD/vibecad_tests/test_cooperative_document_execution.py +++ b/src/Mod/VibeCAD/vibecad_tests/test_cooperative_document_execution.py @@ -6,10 +6,12 @@ import pytest +import VibeCADCooperativeExecution as cooperative from VibeCADCooperativeExecution import ( CooperativeExecutionCancelled, run_document_thread_steps, ) +from VibeCADPerformance import PerformanceRecorder def test_document_steps_return_to_dispatcher_between_every_slice() -> None: @@ -92,6 +94,48 @@ def steps(): assert progress[1]["completed"] == 1 +def test_document_steps_emit_opt_in_gui_spans_with_operation_identity( + monkeypatch, +) -> None: + recorder = PerformanceRecorder(enabled=True, capacity=10) + monkeypatch.setattr(cooperative, "_document_thread_span_factory", recorder.span) + + def steps(): + yield {"phase": "publication_objects", "completed": 1, "total": 1} + return {"ok": True} + + result = run_document_thread_steps( + steps(), + dispatch=lambda operation: operation(), + trace_attributes={ + "operation_id": "job-1", + "document_uid": "document-a", + "capability": "vibescript.publish", + }, + ) + + assert result == {"ok": True} + events = recorder.snapshot() + assert [event["name"] for event in events] == [ + "document.apply_slice", + "document.apply_slice", + ] + assert [event["args"]["slice_index"] for event in events] == [0, 1] + assert events[0]["args"] == { + "operation_id": "job-1", + "document_uid": "document-a", + "capability": "vibescript.publish", + "slice_index": 0, + "phase": "publication_objects", + "completed": 1, + "total": 1, + "gui_thread": True, + "outcome": "completed", + } + assert events[1]["args"]["completed_operation"] is True + assert events[1]["args"]["gui_thread"] is True + + def test_progress_failure_closes_steps_on_the_document_thread() -> None: dispatch_active = False finalized = [] diff --git a/src/Mod/VibeCAD/vibecad_tests/test_performance.py b/src/Mod/VibeCAD/vibecad_tests/test_performance.py index 76ea8e0fe..642b048ff 100644 --- a/src/Mod/VibeCAD/vibecad_tests/test_performance.py +++ b/src/Mod/VibeCAD/vibecad_tests/test_performance.py @@ -4,6 +4,7 @@ import json +import VibeCADCooperativeExecution as cooperative from VibeCADPerformance import EventLoopWatchdog, PerformanceRecorder import VibeCADPerformanceGui @@ -269,3 +270,26 @@ def timer_factory(parent): assert watchdog.ticks == 1 assert VibeCADPerformanceGui.event_loop_watchdog_summary() == {"sample_count": 1} VibeCADPerformanceGui._reset_event_loop_watchdog_for_tests() + + +def test_gui_watchdog_connects_opt_in_document_slice_tracing(monkeypatch): + VibeCADPerformanceGui._reset_event_loop_watchdog_for_tests() + recorder = PerformanceRecorder(enabled=True) + monkeypatch.setattr( + VibeCADPerformanceGui, + "get_performance_recorder", + lambda: recorder, + ) + + timer = VibeCADPerformanceGui.install_event_loop_watchdog( + parent="main-window", + timer_factory=_Timer, + ) + + assert timer is not None + span_factory = cooperative._document_thread_span_factory + assert span_factory is not None + assert span_factory.__self__ is recorder + assert span_factory.__func__ is recorder.span.__func__ + VibeCADPerformanceGui._reset_event_loop_watchdog_for_tests() + assert cooperative._document_thread_span_factory is None diff --git a/src/Mod/VibeCAD/vibecad_tests/test_vibescript_publication_progress.py b/src/Mod/VibeCAD/vibecad_tests/test_vibescript_publication_progress.py index 609f202a3..7157ced96 100644 --- a/src/Mod/VibeCAD/vibecad_tests/test_vibescript_publication_progress.py +++ b/src/Mod/VibeCAD/vibecad_tests/test_vibescript_publication_progress.py @@ -209,6 +209,7 @@ def test_domain_adapter_cooperatively_dispatches_each_publication_step( dispatches = [] events = [] + trace_attributes = None def publication_steps(*_args, **_kwargs): yield {"event": "publication_slice", "completed": 1, "total": 2} @@ -216,13 +217,24 @@ def publication_steps(*_args, **_kwargs): return {"ok": True, "outputs": ["Model"]} monkeypatch.setattr(runtime, "iter_publish_candidate", publication_steps) + run_document_thread_steps = runtime.run_document_thread_steps + + def run_traced_steps(*args, **kwargs): + nonlocal trace_attributes + trace_attributes = kwargs.get("trace_attributes") + return run_document_thread_steps(*args, **kwargs) + + monkeypatch.setattr(runtime, "run_document_thread_steps", run_traced_steps) adapter = runtime.DeclarativeDomainAdapter( SimpleNamespace(domain="assembly", workbench="Assembly") ) result = adapter.publish_cooperatively( object(), - {}, + { + "attempt_id": "attempt-a", + "document_uid": "document-a", + }, {}, document_thread_dispatch=lambda operation: (dispatches.append(operation), operation())[1], cancellation_check=lambda: False, @@ -232,6 +244,12 @@ def publication_steps(*_args, **_kwargs): assert result == {"ok": True, "outputs": ["Model"]} assert len(dispatches) == 3 assert [event["completed"] for event in events] == [1, 2] + assert trace_attributes == { + "operation_id": "attempt-a", + "document_uid": "document-a", + "capability": "vibescript.publish", + "domain": "assembly", + } def test_large_assembly_publication_dispatches_all_307_members(