diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 15cf5685..5cdf7d99 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -268,6 +268,28 @@ 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. +The first measured optimization checkpoint reuses exact rendered icons during +one projection refresh and caches named SVG rendering by resolved path, device- +pixel size, and color map. It does not add persistent projection state or alter +any public API. The same fixture and command produced: + +| Measurement | Baseline | Reuse checkpoint | Change | +|---|---:|---:|---:| +| Correct publication | passed | passed | unchanged | +| Longest cooperative apply unit | 93.518 ms | 93.870 ms | +0.4% | +| GUI heartbeat maximum | 1,863.733 ms | 644.622 ms | -65.4% | +| Final Tree stable refresh | 963.229 ms | 326.063 ms | -66.1% | +| Model-browser rebuild | 473.543 ms | 12.956 ms | -97.3% | +| Browser item creation | 463.308 ms / 321 | 4.273 ms / 321 | -99.1% | +| Tree status-icon generation | 912.865 ms / 642 | 287.627 ms / 321 | -68.5% | +| Final Feature Timeline rebuild | 842.690 ms | 261.887 ms | -68.9% | +| Timeline item construction | 834.236 ms / 312 | 256.264 ms / 312 | -69.3% | +| Timeline icon transformation | 393.617 ms / 312 | 2.351 ms / 6 | -99.4% | + +This is a successful reduction, not completion: the 645 ms final gap still +fails the 100 ms watchdog. The remaining measured Tree and Timeline widget work +must be cooperatively batched without moving Qt widgets off the GUI thread. + ## Work plan and ledger Status values are `DONE`, `IN PROGRESS`, `NOT STARTED`, `BLOCKED`, and @@ -288,7 +310,7 @@ Status values are `DONE`, `IN PROGRESS`, `NOT STARTED`, `BLOCKED`, and | 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 | Representative B07 native trace isolates Tree status-icon generation (912.865 ms) and Timeline icon transformation (393.617 ms); optimization remains | +| P13 | Measure and optimize Tree projection and Feature Timeline rebuild | IN PROGRESS | B07 exact-icon/SVG reuse cuts the final gap 65.4% (1,863.733 to 644.622 ms), browser item creation 99.1%, and Timeline icon transforms 99.4%; bounded widget batching 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/BitmapFactory.cpp b/src/Gui/BitmapFactory.cpp index 86e1c311..254b8ce6 100644 --- a/src/Gui/BitmapFactory.cpp +++ b/src/Gui/BitmapFactory.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -47,10 +48,13 @@ using namespace Gui; namespace Gui { +using SvgPixmapCacheKey = std::tuple; + class BitmapFactoryInstP { public: QMap xpmCache; + std::map svgCache; bool useIconTheme; }; @@ -319,15 +323,26 @@ QPixmap BitmapFactoryInst::pixmapFromSvg( } if (!iconPath.isEmpty()) { + const QSize renderSize = (size * dpr).toSize(); + SvgPixmapCacheKey cacheKey { + iconPath, + renderSize.width(), + renderSize.height(), + colorMapping, + }; + if (const auto cached = d->svgCache.find(cacheKey); cached != d->svgCache.end()) { + return cached->second; + } QFile file(iconPath); if (file.open(QFile::ReadOnly | QFile::Text)) { QByteArray content = file.readAll(); - icon = pixmapFromSvg(content, size * dpr, colorMapping); + icon = pixmapFromSvg(content, renderSize, colorMapping); } - } - if (!icon.isNull()) { - icon.setDevicePixelRatio(dpr); + if (!icon.isNull()) { + icon.setDevicePixelRatio(dpr); + d->svgCache.emplace(std::move(cacheKey), icon); + } } return icon; diff --git a/src/Gui/FeatureTimeline.cpp b/src/Gui/FeatureTimeline.cpp index cf9aff12..2b4ce1b5 100644 --- a/src/Gui/FeatureTimeline.cpp +++ b/src/Gui/FeatureTimeline.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -1305,6 +1306,7 @@ void FeatureTimeline::rebuild() std::vector visibleBlocks; visibleBlocks.reserve(operations.size()); int lastActiveVisible = -1; + std::map, QIcon> iconCache; for (std::size_t index = 0; index < operations.size(); ++index) { if (!isVisibleTimelineOperation(operations[index], internalTransformations, projection)) { continue; @@ -1433,14 +1435,21 @@ void FeatureTimeline::rebuild() Gui::Application::Instance->getViewProvider(iconObject) )) { const QIcon icon = viewProvider->getIcon(); - item->setIcon( - timelineObjectIcon( - icon, - object, - afterPosition, - presentationVisible - ) - ); + const unsigned int iconState = + (afterPosition ? 1U : 0U) + | (presentationVisible.has_value() ? 2U : 0U) + | (presentationVisible.value_or(false) ? 4U : 0U) + | (object->isError() ? 8U : 0U) + | ((object->isTouched() || object->mustExecute() == 1) ? 16U : 0U); + const auto key = std::make_pair(icon.cacheKey(), iconState); + auto cached = iconCache.find(key); + if (cached == iconCache.end()) { + cached = iconCache.emplace( + key, + timelineObjectIcon(icon, object, afterPosition, presentationVisible) + ).first; + } + item->setIcon(cached->second); } } diff --git a/src/Gui/Tree.cpp b/src/Gui/Tree.cpp index a96c7ee8..8f139f97 100644 --- a/src/Gui/Tree.cpp +++ b/src/Gui/Tree.cpp @@ -5555,7 +5555,19 @@ DocumentObjectItem* DocumentItem::createBrowserObjectItem( ); item->populated = true; item->setChildIndicatorPolicy(QTreeWidgetItem::DontShowIndicator); - item->testStatus(true); + QIcon normalIcon; + QIcon disabledIcon; + const bool visible = object->Visibility.getValue(); + for (auto* existingItem : data->items) { + if (existingItem == item || existingItem->isBrowserProxy() + || TreeWidget::objectItemVisibility(existingItem) != visible) { + continue; + } + QIcon& existingIcon = visible ? normalIcon : disabledIcon; + existingIcon = existingItem->icon(0); + break; + } + item->testStatus(true, normalIcon, disabledIcon); return item; }