Skip to content

perf(logging): reuse thread-local buffer for JSON log serialization#8730

Open
danteboe wants to merge 5 commits into
AppFlowy-IO:mainfrom
danteboe:perf/thread-local-logging
Open

perf(logging): reuse thread-local buffer for JSON log serialization#8730
danteboe wants to merge 5 commits into
AppFlowy-IO:mainfrom
danteboe:perf/thread-local-logging

Conversation

@danteboe

@danteboe danteboe commented May 16, 2026

Copy link
Copy Markdown

Motivation

  • High-frequency logging caused many short-lived allocations when serializing events to JSON.

What changed

  • Added a thread_local! LOG_BUFFER: RefCell<Vec<u8>> and rewrote serialization to reuse that buffer per-thread, clearing it between uses and writing JSON directly into it.

Performance impact

  • Reduces short-lived allocation pressure on hot logging paths and improves throughput.

Testing

  • Verified basic log output format.
  • Recommend running cargo check/cargo clippy and sample workload.

Risk and compatibility

  • Medium: threading behavior and buffer reuse are careful to clear data each call. Ensure no callers expect ownership of the returned Vec.

Checklist

  • cargo test / cargo clippy
  • Reviewer: backend/rust, infra/logging

Summary by Sourcery

Optimize logging and related code paths to reduce allocations and improve performance while adjusting UI breadcrumbs caching and housekeeping configs/assets.

New Features:

  • Add support for the perf commit type in commit linting rules.

Bug Fixes:

  • Prevent unnecessary rebuilds of the view title bar breadcrumbs by caching computed widgets in a stateful widget.

Enhancements:

  • Reuse a thread-local JSON serialization buffer in the Rust logging layer to reduce allocations on hot log paths.
  • Avoid intermediate Vec<String> allocations when serializing RAG IDs in chat persistence by streaming UUIDs directly into a JSON serializer.

Chores:

  • Remove unused configuration, environment, and asset files from the repository.
danteboe added 5 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>
@sourcery-ai

sourcery-ai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Optimizes JSON logging performance by reusing a thread-local buffer in Rust, adds caching for Flutter view title breadcrumbs, streamlines UUID list serialization in chat persistence, and updates commit message types while removing several unused configuration/assets files.

Sequence diagram for thread-local JSON logging buffer reuse

sequenceDiagram
  participant Application
  participant FlowyFormattingLayer
  participant LOG_BUFFER
  participant MakeWriter
  participant Writer

  Application->>FlowyFormattingLayer: on_event(event, ctx)
  FlowyFormattingLayer->>LOG_BUFFER: with(closure)
  activate LOG_BUFFER
  LOG_BUFFER-->>FlowyFormattingLayer: borrow_mut()
  deactivate LOG_BUFFER
  FlowyFormattingLayer->>FlowyFormattingLayer: clear buffer
  FlowyFormattingLayer->>FlowyFormattingLayer: serde_json::Serializer::new(buffer)
  FlowyFormattingLayer->>FlowyFormattingLayer: serialize_map / serialize_entry
  FlowyFormattingLayer->>FlowyFormattingLayer: buffer.write_all("\n")
  FlowyFormattingLayer->>MakeWriter: make_writer()
  MakeWriter-->>FlowyFormattingLayer: Writer
  FlowyFormattingLayer->>Writer: write_all(buffer)
  Writer-->>FlowyFormattingLayer: Result
  FlowyFormattingLayer-->>Application: return
Loading

File-Level Changes

Change Details Files
Reuse a thread-local buffer for JSON log serialization instead of allocating a new Vec per log/span event and write directly to the writer.
  • Introduce a thread-local LOG_BUFFER: RefCell<Vec> initialized with a 1KB capacity for reuse across log calls.
  • Change span formatting helper to operate in-place on the shared buffer, appending a newline and writing directly to the MakeWriter instead of returning Vec.
  • Refactor event logging path to use the same LOG_BUFFER, building JSON into the shared buffer, appending a newline, and writing directly via make_writer, bypassing the previous emit(Vec) flow.
frontend/rust-lib/lib-log/src/layer.rs
Cache breadcrumb widgets in the Flutter ViewTitleBar to avoid rebuilding them on every rebuild when inputs are unchanged.
  • Convert ViewTitleBar from StatelessWidget to StatefulWidget and introduce a private _ViewTitleBarState to hold cache state.
  • Maintain a cached list of breadcrumb widgets and a derived cache key string that encodes ancestor IDs and relevant state flags.
  • Invalidate the cache in didUpdateWidget when the view identity or name changes and reuse the cached breadcrumb widgets in build when the key is unchanged.
frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart
Avoid intermediate Vec allocation when serializing rag_ids in chat persistence by streaming UUIDs directly into a JSON serializer.
  • Introduce a helper function serialize_rag_ids_from_uuids that takes &[Uuid] and writes a JSON array of stringified UUIDs via serde_json::Serializer into a Vec.
  • Replace the previous rag_ids: Vec-based serialization in ChatTable::new with the new streaming helper and wrap the result in Some(...).
frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs
Allow performance-related commit messages and clean up unused or committed-by-mistake files.
  • Extend commitlint type-enum to include "perf" as an allowed commit type.
  • Delete unused or environment-specific files such as a Rust .cargo config, sqlite .env, a translation file, and build/cache artifacts.
commitlint.config.js
frontend/appflowy_flutter/assets/translations/mr-IN.json
frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json
frontend/rust-lib/.cargo/config.toml
frontend/rust-lib/flowy-sqlite/.env
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 1 issue, and left some high level feedback:

  • In the Rust logging layer, LOG_BUFFER is borrowed mutably for the entire serialization in both the span and event paths while calling tracing::debug! inside that scope; if those debug logs hit the same layer they can re-enter LOG_BUFFER.with and trigger a RefCell borrow panic—consider removing or deferring those debug logs, or separating the reserved-field logging from the hot path.
  • Now that span and event formatting write directly to the writer using the thread-local buffer, the emit(&self, Vec<u8>) helper appears unused and could be removed or repurposed to avoid having two different emission paths.
  • In ChatTable::new, the nested serialize_rag_ids_from_uuids helper is recreated on every call; consider hoisting it to a private function or method (and potentially using serde_json::to_string with a wrapper type) to avoid per-call function definition overhead and to keep serialization logic reusable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the Rust logging layer, `LOG_BUFFER` is borrowed mutably for the entire serialization in both the span and event paths while calling `tracing::debug!` inside that scope; if those debug logs hit the same layer they can re-enter `LOG_BUFFER.with` and trigger a `RefCell` borrow panic—consider removing or deferring those debug logs, or separating the reserved-field logging from the hot path.
- Now that span and event formatting write directly to the writer using the thread-local buffer, the `emit(&self, Vec<u8>)` helper appears unused and could be removed or repurposed to avoid having two different emission paths.
- In `ChatTable::new`, the nested `serialize_rag_ids_from_uuids` helper is recreated on every call; consider hoisting it to a private function or method (and potentially using `serde_json::to_string` with a wrapper type) to avoid per-call function definition overhead and to keep serialization logic reusable.

## Individual Comments

### Comment 1
<location path="frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs" line_range="43-44" />
<code_context>
+      String::from_utf8(buf).unwrap_or_default()
+    }
+
     let metadata = serialize_chat_metadata(&metadata);
-    let rag_ids = Some(serialize_rag_ids(&rag_ids));
+    let rag_ids = Some(serialize_rag_ids_from_uuids(&rag_ids));
     Self {
       chat_id,
</code_context>
<issue_to_address>
**suggestion:** New `serialize_rag_ids_from_uuids` duplicates existing serialization logic instead of reusing it.

The new helper manually builds JSON and is only used here, while `serialize_rag_ids` already centralizes this logic. This creates two code paths for the same behavior and risks inconsistencies.

Please either reuse `serialize_rag_ids` by generalizing it to work with `Uuid` (or `Display`), or colocate both helpers and have one delegate to the other so the JSON format is defined in a single place.

Suggested implementation:

```rust
      // Convert UUIDs to strings and delegate to the existing serializer
      let rag_id_strings: Vec<String> = rag_ids.iter().map(|id| id.to_string()).collect();
      serialize_rag_ids(&rag_id_strings)
    }

    let metadata = serialize_chat_metadata(&metadata);
    let rag_ids = Some(serialize_rag_ids_from_uuids(&rag_ids));

```

1. Ensure the signature of `serialize_rag_ids_from_uuids` matches this implementation (e.g. `fn serialize_rag_ids_from_uuids(rag_ids: &[Uuid]) -> String` or similar).
2. Confirm the existing `serialize_rag_ids` function accepts `&[String]` or `&Vec<String>`; if it expects an owned `Vec<String>`, remove the reference (`serialize_rag_ids(rag_id_strings)`).
3. If `serialize_rag_ids` currently has a different parameter type, adjust either its parameter type or add a small adapter so it can accept `&[String]` without changing its behavior.
</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 43 to +44
let metadata = serialize_chat_metadata(&metadata);
let rag_ids = Some(serialize_rag_ids(&rag_ids));
let rag_ids = Some(serialize_rag_ids_from_uuids(&rag_ids));

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: New serialize_rag_ids_from_uuids duplicates existing serialization logic instead of reusing it.

The new helper manually builds JSON and is only used here, while serialize_rag_ids already centralizes this logic. This creates two code paths for the same behavior and risks inconsistencies.

Please either reuse serialize_rag_ids by generalizing it to work with Uuid (or Display), or colocate both helpers and have one delegate to the other so the JSON format is defined in a single place.

Suggested implementation:

      // Convert UUIDs to strings and delegate to the existing serializer
      let rag_id_strings: Vec<String> = rag_ids.iter().map(|id| id.to_string()).collect();
      serialize_rag_ids(&rag_id_strings)
    }

    let metadata = serialize_chat_metadata(&metadata);
    let rag_ids = Some(serialize_rag_ids_from_uuids(&rag_ids));
  1. Ensure the signature of serialize_rag_ids_from_uuids matches this implementation (e.g. fn serialize_rag_ids_from_uuids(rag_ids: &[Uuid]) -> String or similar).
  2. Confirm the existing serialize_rag_ids function accepts &[String] or &Vec<String>; if it expects an owned Vec<String>, remove the reference (serialize_rag_ids(rag_id_strings)).
  3. If serialize_rag_ids currently has a different parameter type, adjust either its parameter type or add a small adapter so it can accept &[String] without changing its behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants