Skip to content

Add retained-scene refresh profiling and guides#486

Merged
wieslawsoltes merged 22 commits into
masterfrom
codex/retained-scene-perf-docs
Apr 14, 2026
Merged

Add retained-scene refresh profiling and guides#486
wieslawsoltes merged 22 commits into
masterfrom
codex/retained-scene-perf-docs

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

@wieslawsoltes wieslawsoltes commented Apr 11, 2026

Summary

This PR adds retained-scene mutation refresh APIs to SKSvg, a dedicated SVG pipeline profiler plus BenchmarkDotNet coverage for the hot phases, a larger retained-scene/how-to documentation set, and a follow-up optimization pass that reduces parser, retained-scene, text-layout, native picture, and bitmap-render overhead without dropping SVG feature support.

Changes

Runtime and optimization work

  • added TryApplyRetainedSceneMutationAndRender(...) overloads for direct element, address-key, and id-based retained-scene refresh flows in SKSvg
  • refreshed the active Picture directly from the retained scene when an incremental mutation succeeds instead of forcing a full FromSvgDocument(...) rebuild
  • added SvgLoadPipelineProfiler and wired it into tests/Svg.Skia.Benchmarks via --profile-svg
  • added BenchmarkDotNet suites for load-pipeline phases, retained-compile subphases, native picture creation, bitmap rendering, text compile internals, aligned text placement, and textPath placement
  • reused already-computed text bounds during text-scene compilation instead of measuring them twice
  • aggregated mixed-content node text in SvgDocumentCompatibilityLoader in a single pass to cut parser-side allocations and repeated enumeration
  • extracted inline-style parsing into a dedicated SvgInlineStyleAttributeParser so SvgElementFactory stays focused on XML attribute dispatch
  • cached retained-scene element address keys during compile, index rebuild, and dependency resolution to avoid repeated ancestor walks and sibling IndexOf(...) scans
  • precomputed SvgElementAddress.Key without LINQ so key materialization is not repeated on every access
  • added safe text-layout fast paths for spacing-free runs, exact guarded codepoint-advance measurement, and faster textPath placement traversal
  • safely reused nested native SKShader, SKColorFilter, SKPathEffect, and SKImageFilter state for complex paints while keeping mutable picture-backed paths on the uncached route
  • skipped unnecessary dependency/resource rebuild work during retained-scene refresh when the source document does not actually reference those structures
  • moved retained runtime payload ownership to the rebuild phase so structural nodes avoid unnecessary fill/stroke paint creation while root/container opacity still applies correctly
  • stored direct primitive visuals as raw local path/paint payloads instead of recording per-shape nested shim pictures, and updated the retained-scene renderer/animation-layer path to draw those visuals directly

Correctness and regression coverage

  • added regression coverage for retained-scene mutation refresh updating the active picture
  • added retained-scene parity tests for primitive shapes, root opacity, filtered groups, and long multi-segment textPath content
  • added text-layout regressions covering ASCII, combining marks, kerning pairs, spacing, and textPath
  • fixed follow-up regressions around animation-layer cache invalidation after retained mutations, BaseUri handling in the profiler, inline-style CSS property casing, and unstable temporary <use> addresses during retained-scene key caching

Documentation

  • expanded the loading guide to cover file, stream, reader, raw string, SvgDocument, reload, and VectorDrawable entry points
  • added dedicated guides for SvgDocument handling and mutation, retained scene graph usage, and performance/retained-scene refresh
  • rewrote the hit-testing guide around HitTestElements(...), HitTestSceneNodes(...), and topmost-target APIs
  • refreshed the rendering-pipeline and picture-model concept pages to describe the current SourceDocument -> retained scene -> shim model -> native picture flow
  • updated Svg.Skia, Svg.Model, and Svg.Editor.Skia package pages to point at the new guides and current terminology

Performance notes

Profiled with:

dotnet run --project tests/Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj -c Release --no-build -- --profile-svg '/Users/wieslawsoltes/Downloads/solar battery.svg' 30

Current osx-arm64 Release profiler values for /Users/wieslawsoltes/Downloads/solar battery.svg:

Stage Mean
Parse SvgDocument from string 1.72 ms
Compile retained scene 4.58 ms
Create shim picture model 0.03 ms
Create native SKPicture 0.60 ms
Render native picture to bitmap 3.69 ms
Encode native picture to PNG 14.86 ms
Load via SKSvg.FromSvg(...) 11.66 ms
Control-like source load 13.43 ms
Mutate + full FromSvgDocument(...) rebuild 19.54 ms
Mutate + retained-scene refresh 12.41 ms

Against the original five-stage baseline on this asset, the current branch moved from 15.34 ms to 10.62 ms for parse + retained compile + shim model + native picture + bitmap render, which is about 30.8% faster overall.

Representative targeted BenchmarkDotNet deltas on the same asset:

  • retained compile CompileNodeTreeOnly: 2.061 ms -> 1.782 ms (-13.5%)
  • retained compile RegisterDependenciesOnly: 369.7 us -> 328.5 us (-11.1%)
  • retained compile ResolveRuntimePayloadsOnly: 149.1 us -> 137.6 us (-7.7%)
  • native render DrawNativePicture1x: 3.803 ms -> 3.397 ms (-10.7%)
  • native render RenderTransparentBitmap1x: 3.672 ms -> 3.498 ms (-4.7%)
  • native render RenderOpaqueBitmap1x: 3.773 ms -> 3.525 ms (-6.6%)

Main takeaway: retained-scene compile remains the largest in-pipeline SKSvg load cost for this asset, but the branch removes repeated work across parsing, retained-scene indexing/dependency rebuild, text placement, native picture construction, and bitmap rendering while keeping incremental retained-scene refresh below a full FromSvgDocument(...) rebuild after DOM edits.

Validation

  • dotnet tool run lunet --stacktrace build
  • dotnet build tests/Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj -c Release --no-restore
  • dotnet test tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj -f net10.0 -c Release --no-restore --filter "FullyQualifiedName~CreateRetainedSceneGraphPicture_MatchesCurrentPicture_ForPrimitiveShapesDocument|FullyQualifiedName~CreateRetainedSceneGraphPicture_MatchesCurrentPicture_ForRootOpacityDocument|FullyQualifiedName~CreateRetainedSceneGraphPicture_MatchesCurrentPicture_ForFilteredGroupDocument|FullyQualifiedName~CreateRetainedSceneGraphPicture_MatchesCurrentPicture_ForLongTextPathDocument|FullyQualifiedName~RetainedSceneGraph_TextPathPercentStartOffset_FollowsArcGeometry|FullyQualifiedName~resvgTests.e_textPath|FullyQualifiedName~resvgTests.a_letter_spacing|FullyQualifiedName~resvgTests.a_word_spacing|FullyQualifiedName~SvgAnimationControllerTests"
  • targeted BenchmarkDotNet runs for load-pipeline, retained-compile, text-layout, native-picture, and bitmap-render suites

Notes

  • profiler numbers are still somewhat noisy run-to-run on this asset; the hot-path ordering and targeted benchmark deltas are more stable than any single sample
  • the docs build still reports existing unresolved Uno/Windows xrefs during site generation
  • sample/generated-code warnings and older-target package noise remain pre-existing background output

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b813d6e82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Svg.Skia/SKSvg.SceneGraph.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50f4624a4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ShimSkiaSharp/SKImage.cs
Comment thread src/ShimSkiaSharp/SKPath.cs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82a857839e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Svg.SceneGraph/SvgSceneCompiler.cs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8d8a599b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

hash.Add(erodeInputRevision);
break;
case ImageImageFilter imageImageFilter:
hash.Add(imageImageFilter.Image?.Version ?? 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include image bytes in cached ImageImageFilter revision

The ImageImageFilter cache key only hashes image.Version, so in-place edits to SKImage.Data (same array instance) do not change the revision and GetRenderImageFilter(...) can keep returning a stale native filter. This means filter outputs using ImageImageFilter can ignore legitimate runtime image-buffer mutations until callers replace the whole Data array object.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a6a5708eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

hash.Add(erodeInputRevision);
break;
case ImageImageFilter imageImageFilter:
hash.Add(imageImageFilter.Image?.Version ?? 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include image content in ImageImageFilter revision hash

GetRenderImageFilter(...) reuses cached native filters based on TryGetImageFilterRevision(...), but the ImageImageFilter branch only hashes image.Version. SKImage.Version changes on property assignment, not when callers mutate the existing byte[] in place, so filter caches can serve stale pixels after legitimate image.Data buffer edits. This makes filter output incorrect until the image object or data reference is replaced.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 909f953c1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

hash.Add(erodeInputRevision);
break;
case ImageImageFilter imageImageFilter:
hash.Add(imageImageFilter.Image?.Version ?? 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include image content in ImageImageFilter cache revision

TryGetImageFilterRevision hashes ImageImageFilter using only image.Version, but SKImage.Version does not change when callers mutate the existing Data buffer in place. In that case GetRenderImageFilter will reuse a stale native image filter and filtered output will not reflect valid runtime image-byte updates until a new Data array instance is assigned.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce8b3df7ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

get => _color;
set
{
if (_color.Equals(value))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use null-safe equality in SKPaint.Color setter

The Color setter calls _color.Equals(value), but _color is nullable and can legitimately become null after paint.Color = null. Any later assignment (including assigning null again) then throws a NullReferenceException, which turns a normal paint update into a runtime crash path for callers that clear and reapply colors.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant