Skip to content

perf(svg): add simple path+size keyed SvgPicture cache for asset icons#8731

Open
danteboe wants to merge 10 commits into
AppFlowy-IO:mainfrom
danteboe:perf/svg-picture-caching
Open

perf(svg): add simple path+size keyed SvgPicture cache for asset icons#8731
danteboe wants to merge 10 commits into
AppFlowy-IO:mainfrom
danteboe:perf/svg-picture-caching

Conversation

@danteboe

@danteboe danteboe commented May 16, 2026

Copy link
Copy Markdown

Motivation

  • Frequently-requested static SVG assets are reparsed on each use causing CPU and memory overhead.

What changed

  • Added a small in-memory cache keyed by normalized asset path + size to reuse SvgPicture.asset instances when no color filter is requested. Colored cases fall back to normal instantiation.

Performance impact

  • Faster icon reuse and reduced parsing when using static icons without color transforms.

Testing

  • Manual icon rendering checks across themes and sizes.

Risk and compatibility

  • Low: caching is conservative and bypassed for colored icons. Consider LRU or bounded cache in future.

Checklist

  • flutter analyze
  • Reviewer: frontend/ui

Summary by Sourcery

Optimize SVG icon rendering and logging performance while improving breadcrumb rendering and minor persistence and tooling behavior.

Enhancements:

  • Introduce a simple in-memory cache for asset-based SvgPicture widgets to reuse pre-parsed SVG icons when no color filter is applied.
  • Convert the view title bar to a stateful widget with cached breadcrumb items and lazy horizontal scrolling to reduce rebuild and layout overhead.
  • Refine view renaming popover construction to reuse the latest bloc state when available and fall back safely when not.
  • Reuse a thread-local buffer and more efficient string building in the Rust logging layer to reduce allocations when serializing spans and events.
  • Avoid intermediate allocations when serializing chat RAG IDs by writing them directly via a serde JSON sequence serializer.

Chores:

  • Allow perf as a valid commit type in commitlint configuration.
  • Remove unused or generated configuration, environment, translation, and build artifact files from the repository.

danteboe added 10 commits May 15, 2026 18:55
- Detailed architectural explanation: the previous StatelessWidget implementation rebuilt breadcrumb widget instances on every parent rebuild, causing repeated allocations of intermediate FlowyTooltip/ViewTitle/FlowySvg widgets and increasing GC churn on UI re-renders.\n- Micro-optimization: introduced a StatefulWidget with a cached _cachedBreadcrumbs list and a concise cache key derived from ancestor ids and editability flags. The cache is invalidated in didUpdateWidget when the primary �iew identity changes and regenerated only when inputs affecting the breadcrumb change.\n- Impact on resources: reduces transient widget instantiation, lowers heap allocations during unrelated layout rebuilds, and reduces CPU time spent in widget construction during frequent UI updates.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
…tions in view_title_bar

- Architectural explanation: canonicalizing statically parameterized widgets reduces repeated runtime allocations by enabling the Dart compiler to canonicalize identical widget instances at compile-time.\n- Work performed: reviewed �iew_title_bar.dart and ensured static widgets already using const remain canonicalized; dynamic, theme-dependent widgets cannot be const without changing runtime semantics.\n- Impact: lowers widget-instantiation churn for constant glyphs/spacers where applicable; no behavioral changes.
…intermediate allocations

- Detailed architectural explanation: existing code collected UUIDs into a temporary Vec<String> before JSON serialization, causing an extra heap allocation proportional to the number of ids.\n- Optimization: added a streaming serializer serialize_rag_ids_from_uuids that writes the JSON array directly from the Uuid iterator into a byte buffer, avoiding the intermediate Vec<String>.\n- Impact: eliminates the transient allocation for rag id lists, reduces heap churn and shortens peak memory usage during chat persistence operations.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
- Detailed architectural explanation: frequent log serialization previously allocated a new Vec<u8> for each span/event, causing heap churn in high-throughput scenarios.\n- Optimization: introduced a LOG_BUFFER thread-local RefCell<Vec<u8>> reused across serialization calls; buffer is cleared (len=0) but retains capacity between calls. Serialization now writes directly into this buffer and the writer consumes it, avoiding repeated allocations.\n- Impact: reduces heap allocations and GC pressure in hot logging paths; improves throughput for bursty logs.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
…id inline closure allocations

- Architectural explanation: anonymous builder closures allocated during widget rebuilds contribute to transient allocation churn on hot UI paths.\n- Optimization: hoisted the popupBuilder into a _buildRenamePopover method and tracked the latest ViewTitleState in a field so the builder no longer needs an inline anonymous closure.\n- Impact: reduces per-build closure allocations and clarifies lifecycle points for text controller reset logic.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
…rumb rendering

- Detailed architectural explanation: the previous SingleChildScrollView + Row eagerly built all breadcrumb widget instances, causing potentially large widget allocation trees for deep hierarchies.\n- Optimization: replaced with a horizontally scrolling ListView.separated which builds items on demand. Each item uses ValueKey(view.id) to maintain identity. Separators are provided by separatorBuilder to match previous dividers.\n- Impact: converts O(n) eager widget construction to on-demand item builders, reducing initial layout cost and memory pressure when the breadcrumb list is long.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
…er with write! to avoid nested format allocations

- Architectural explanation: nested ormat! calls create transient Strings which increase heap churn during high-frequency logging.\n- Optimization: replaced nested ormat! usage with write! into preallocated Strings and combined span/context formatting to reduce intermediate allocations. Integrated with the existing thread-local buffer to minimize allocations further.\n- Impact: reduces temporary String allocations on hot logging paths and improves throughput.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
- Added a lightweight in-memory cache for SvgPicture.asset instances keyed by normalized asset path and requested size.\n- Cache is bypassed when a color filter is requested to ensure correct coloring semantics.\n- This reduces repeated parse/asset lookup work for frequently used static icons.\n- Kept implementation intentionally simple and synchronous to avoid lifecycle complexity; can be extended to LRU or weak ref cache later.\n\nCo-authored-by: Optimization-Agent <agent@flowy.ai>
- Add use std::fmt::Write as FmtWrite; to ensure write! into String resolves to the fmt::Write impl and avoids ambiguity with std::io::Write.\n- This fixes a likely compile-time error seen in CI after replacing nested ormat! calls.
@sourcery-ai

sourcery-ai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a small in-memory cache for asset-based SvgPicture icons and several related micro-optimizations across Flutter and Rust (breadcrumb rendering, logging, chat persistence), plus commit type support for perf, to reduce allocations and CPU overhead in hot paths.

File-Level Changes

Change Details Files
Add simple size+path keyed cache for SvgPicture.asset usages in FlowySvg to avoid reparsing static SVG icons without color filters.
  • Introduce a global Map-backed cache keyed by asset path and size for SvgPicture instances.
  • Add _getOrCreateCachedSvg helper that constructs or reuses cached SvgPicture.asset widgets.
  • Update FlowySvg build logic to use the cache only when no iconColor/blendMode is applied, and fall back to uncached SvgPicture.asset when color filters are needed.
frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart
Optimize view title bar breadcrumbs and rename popover state handling in the Flutter workspace UI.
  • Convert ViewTitleBar from StatelessWidget to StatefulWidget with internal cached breadcrumb widgets and a key based on ancestors and access state, invalidated when the view changes.
  • Replace eager SingleChildScrollView+Row breadcrumb layout with a horizontally scrolling ListView.separated for lazier item build and explicit separators.
  • Track the latest ViewTitleState inside _ViewTitleState and factor a _buildRenamePopover helper so the popover can reuse the most recent bloc state, falling back to reading the bloc when needed.
frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart
Reduce allocations and reuse buffers in Rust logging layer for span/event JSON formatting.
  • Add a thread-local Vec LOG_BUFFER for serializing log records without per-call allocations.
  • Change span serialization to write directly into LOG_BUFFER, append a newline, and write to the MakeWriter instead of returning a Vec.
  • Rewrite format_span_context and format_event_message to use preallocated Strings and write! instead of nested format! calls.
  • Update event formatting to reuse LOG_BUFFER and write directly to the writer, eliminating the intermediate emit path for spans and events.
frontend/rust-lib/lib-log/src/layer.rs
Avoid intermediate Vec allocation when serializing rag_ids in chat persistence.
  • Introduce a small helper that serializes &[Uuid] into a JSON array string using serde_json::Serializer and serialize_seq.
  • Update ChatTable::new to call the new helper directly on the Uuid slice and wrap the result in Some(...) for rag_ids.
frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs
Allow perf as a valid commit type and clean up unused/generated project files.
  • Extend commitlint type-enum to include 'perf' to reflect performance-oriented commits.
  • Remove unused or generated config and build artifacts (.cargo config, sqlite .env, iOS build cache JSON) and an obsolete translation file, while touching some asset/license/translation fixtures without semantic changes.
commitlint.config.js
frontend/appflowy_flutter/assets/translations/mr-IN.json
frontend/rust-lib/.cargo/config.toml
frontend/rust-lib/flowy-sqlite/.env
frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json
frontend/appflowy_flutter/assets/google_fonts/Poppins/OFL.txt
frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt
frontend/resources/translations/ur.json
frontend/rust-lib/event-integration-test/tests/asset/project.csv

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@CLAassistant

CLAassistant commented May 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The new SvgPicture cache currently returns the same widget instance for identical path/size, which will break if the same icon is mounted in multiple places at once—consider caching a PictureProvider or pre-parsed data and creating a fresh SvgPicture per use instead of reusing the widget.
  • In ViewTitleBar, the new ListView-based breadcrumb rendering omits the trailing HSpace and _buildLockPageStatus that were previously rendered after the titles; double-check if dropping the lock indicator is intentional or restore it in the new layout.
  • The new serialize_rag_ids_from_uuids helper in chat_sql.rs introduces several unwrap/unwrap_or_default calls during serialization, which can panic or silently mask encoding errors at runtime; consider returning a Result or otherwise handling these failures more defensively.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new SvgPicture cache currently returns the same widget instance for identical path/size, which will break if the same icon is mounted in multiple places at once—consider caching a PictureProvider or pre-parsed data and creating a fresh SvgPicture per use instead of reusing the widget.
- In ViewTitleBar, the new ListView-based breadcrumb rendering omits the trailing HSpace and _buildLockPageStatus that were previously rendered after the titles; double-check if dropping the lock indicator is intentional or restore it in the new layout.
- The new serialize_rag_ids_from_uuids helper in chat_sql.rs introduces several unwrap/unwrap_or_default calls during serialization, which can panic or silently mask encoding errors at runtime; consider returning a Result or otherwise handling these failures more defensively.

## Individual Comments

### Comment 1
<location path="frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart" line_range="103-105" />
<code_context>
-                        pageAccessLevelState,
+
+              // Build a cache key from inputs that influence breadcrumb widgets.
+              final currentKey = '${ancestors.map((a) => a.id).join(',')}|${state.isDeleted}|${pageAccessLevelState.isEditable}|${pageAccessLevelState.sectionType.name}';
+
+              if (_cachedKey != currentKey || _cachedBreadcrumbs.isEmpty) {
+                _cachedBreadcrumbs = _buildViewTitles(
+                  context,
</code_context>
<issue_to_address>
**issue (bug_risk):** Breadcrumb cache key risks stale UI when ancestor titles or other properties change.

The key only uses ancestor IDs and a few flags, not names or other visual properties. If an ancestor is renamed (or similar display properties change) without its `id` changing, the key stays the same and `_cachedBreadcrumbs` is reused, so labels can go stale. In addition, `_cachedBreadcrumbs` is never read in the `ListView` (you rebuild from `ancestors`), so this cache appears unused or misleading. Either (a) actually use the cache in the `itemBuilder` and include all breadcrumb‑affecting properties in the key, or (b) remove the cache and rely on `ListView.separated` to rebuild as needed.
</issue_to_address>

### Comment 2
<location path="frontend/rust-lib/lib-log/src/layer.rs" line_range="76-81" />
<code_context>
     span: &SpanRef<'a, S>,
     ty: Type,
     ctx: &Context<'_, S>,
-  ) -> Result<Vec<u8>, std::io::Error> {
-    let mut buffer = Vec::new();
-    let mut serializer = serde_json::Serializer::new(&mut buffer);
-    let mut map_serializer = serializer.serialize_map(None)?;
-    let message = format_span_context(span, ty, ctx);
-    self.serialize_fields(&mut map_serializer, &message, span.metadata().level())?;
-    if self.with_target {
-      map_serializer.serialize_entry("target", &span.metadata().target())?;
-    }
-
-    // map_serializer.serialize_entry("line", &span.metadata().line())?;
-    // map_serializer.serialize_entry("file", &span.metadata().file())?;
+  ) -> Result<(), std::io::Error> {
+    LOG_BUFFER.with(|buf| {
+      let mut b = buf.borrow_mut();
</code_context>
<issue_to_address>
**suggestion:** Changed `serialize_span` to write directly but left `emit` as a now-unnecessary helper.

Now that `serialize_span` writes directly to the writer and returns `Result<(), std::io::Error>`, `emit` is no longer used for spans, and events also write directly. Keeping `emit` with its `Vec<u8>` signature is confusing and implies a dead code path. Please either remove `emit` or refactor spans and events to route through a shared emission function for consistency.

Suggested implementation:

```rust

```

1. If the `emit` function body differs from the one I’ve shown (e.g. different variable names, missing `flush`, additional logic), adjust the `SEARCH` block to match the exact existing function definition so it can be removed cleanly.
2. Ensure there are no remaining call sites to `emit`. If any still exist, inline the write/flush logic directly at those call sites (using `self.make_writer.make_writer()` with `write_all` and `flush`) to keep behavior consistent with spans and events writing directly.
3. If you want a shared emission path in the future, it would now be clearer to introduce a new helper with a signature aligned to the “write directly” pattern (e.g. taking a closure that serializes into a `Write`), rather than the old `Vec<u8>`-based `emit`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +103 to +105
final currentKey = '${ancestors.map((a) => a.id).join(',')}|${state.isDeleted}|${pageAccessLevelState.isEditable}|${pageAccessLevelState.sectionType.name}';

if (_cachedKey != currentKey || _cachedBreadcrumbs.isEmpty) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Breadcrumb cache key risks stale UI when ancestor titles or other properties change.

The key only uses ancestor IDs and a few flags, not names or other visual properties. If an ancestor is renamed (or similar display properties change) without its id changing, the key stays the same and _cachedBreadcrumbs is reused, so labels can go stale. In addition, _cachedBreadcrumbs is never read in the ListView (you rebuild from ancestors), so this cache appears unused or misleading. Either (a) actually use the cache in the itemBuilder and include all breadcrumb‑affecting properties in the key, or (b) remove the cache and rely on ListView.separated to rebuild as needed.

Comment on lines -76 to -81
) -> Result<Vec<u8>, std::io::Error> {
let mut buffer = Vec::new();
let mut serializer = serde_json::Serializer::new(&mut buffer);
let mut map_serializer = serializer.serialize_map(None)?;
let message = format_span_context(span, ty, ctx);
self.serialize_fields(&mut map_serializer, &message, span.metadata().level())?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Changed serialize_span to write directly but left emit as a now-unnecessary helper.

Now that serialize_span writes directly to the writer and returns Result<(), std::io::Error>, emit is no longer used for spans, and events also write directly. Keeping emit with its Vec<u8> signature is confusing and implies a dead code path. Please either remove emit or refactor spans and events to route through a shared emission function for consistency.

Suggested implementation:

  1. If the emit function body differs from the one I’ve shown (e.g. different variable names, missing flush, additional logic), adjust the SEARCH block to match the exact existing function definition so it can be removed cleanly.
  2. Ensure there are no remaining call sites to emit. If any still exist, inline the write/flush logic directly at those call sites (using self.make_writer.make_writer() with write_all and flush) to keep behavior consistent with spans and events writing directly.
  3. If you want a shared emission path in the future, it would now be clearer to introduce a new helper with a signature aligned to the “write directly” pattern (e.g. taking a closure that serializes into a Write), rather than the old Vec<u8>-based emit.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants