Skip to content

Keep previews responsive while switching large libraries#16289

Merged
Siedlerchr merged 9 commits into
mainfrom
agent/improve-large-library-switching
Jul 21, 2026
Merged

Keep previews responsive while switching large libraries#16289
Siedlerchr merged 9 commits into
mainfrom
agent/improve-large-library-switching

Conversation

@Siedlerchr

@Siedlerchr Siedlerchr commented Jul 19, 2026

Copy link
Copy Markdown
Member

Related issues and pull requests

Meta-issue: #8906

Depends on JabRef/html-to-node#16.

PR Description

This improves switching between large libraries: automatic group paths are merged with temporary indexes, group counts are loaded only when their cells become visible, and group-count work no longer occupies the preview worker pool. Preview updates are rendered atomically and discard stale background results, keeping the selected entry responsive while groups are recalculated.

The group calcuation is now based on a https://de.wikipedia.org/wiki/Trie

This is a draft until html-to-node PR #16 is merged and its 0.2.0 release is published.

Steps to test

  1. Publish html-to-node PR fixed the bug 960 #16 to Maven local, then run JabRef with ./gradlew -PuseMavenLocal :jabgui:run.
  2. Open two libraries with many entries and automatic groups, then switch between them while selecting entries.
  3. Confirm that the selected entry preview renders promptly and visible group counts appear without blocking it.

The contributor manually performed this scenario with a generated large library. A static screenshot would not demonstrate this performance behavior.

AI usage

OpenAI Codex (model GPT-5) was used for assistance. The contributor reviewed, understood, manually tested, and takes responsibility for this change.

AI CHECKLIST.md walkthrough

1. Code self-review

Nullability and control flow

  • No == null / != null checks — JSpecify annotations (@NullMarked, @Nullable, @NonNull) used instead.
  • No Objects.requireNonNull(...) — nullability expressed via JSpecify annotations.
  • New classes annotated with @NullMarked (org.jspecify.annotations.NullMarked).
  • Optional consumed with ifPresent / ifPresentOrElse / map / orElseThrow — never orElse(unusedValue) nor an isPresent() + get() block.
  • StringUtil.isBlank(...) used instead of s == null || s.isBlank().

Exceptions

  • No catch (Exception e) — only specific exceptions are caught.
  • No throw new RuntimeException(...) / IllegalStateException(...) — these tear down the whole application.
  • Logged exceptions are passed as the last logger argument (LOGGER.info("...", e)), not concatenated into the message string.

Style and idioms

  • [/] New BibEntry objects built with withers (withField, not setField).
  • Modern Java used: List.of() / Map.of() / Set.of(), Path.of(), SequencedCollection / SequencedSet, text blocks.
  • [/] Regexes use a precompiled Pattern.compile(...) constant, not String.matches(...).
  • Background work uses org.jabref.logic.util.BackgroundTask, not new Thread().
  • No commented-out code, no trivial comments restating the code, no AI-disclosure comments in source.
  • Markdown Javadoc (///) uses Markdown syntax, not JavaDoc inline tags: `code` instead of {@code}, [ClassName] instead of {@link}.

User-facing text

  • [/] All user-facing text localized (Localization.lang in Java, % prefix in FXML).
  • [/] Sentence case (not Title Case); no trailing !; labels do not end with :.
  • [/] Variance expressed with placeholders ("...: %0"), not string concatenation.

Security

  • [/] User-controlled data (request params, entry fields, file contents) is HTML-escaped before being written into any text/html response — including exception/error messages, not just the success body (XSS).

Tests

  • Behavior changes in org.jabref.model / org.jabref.logic have added or updated tests.
  • Tests assert object contents (assertEquals), use plain JUnit asserts (not AssertJ), have no @DisplayName, do not catch exceptions (let them propagate so JUnit reports setup/teardown failures directly), and use @TempDir instead of manual temp directories.

2. Verification commands

Run in this order — cheapest first. Each must pass.

  • ./gradlew :jablib:check (or ./gradlew check for all modules). The local run completed test execution but Gradle failed collecting a transient test-result file; CI will perform this check.
  • ./gradlew checkstyleMain checkstyleTest checkstyleJmh.
  • ./gradlew modernizer.
  • ./gradlew --no-configuration-cache :rewriteDryRun reports no changes (run ./gradlew rewriteRun to fix).
  • ./gradlew javadoc.
  • npx markdownlint-cli2 "docs/**/*.md" "*.md" (only if Markdown changed).
  • [/] Only if formatting is still off after rewriteRun: docker run -v $(pwd):/github/workspace ghcr.io/leventebajczi/intellij-format:master "*.java" "" ".idea/codeStyles/Project.xml".

3. Documentation

  • CHANGELOG.md entry added if the change is visible to the user (end-user wording, no extra blank lines). Link the issue if one exists; link the PR only when no issue exists. Use TODO as the placeholder when neither is known yet — never a fake number.
  • Searched jabref/issues and jabref-koppor/issues for a related issue; linked only on a confident match, otherwise kept TODO (no closes/fixes for merely-similar issues).
  • Requirement added to docs/requirements/<area>.md if the change is a new feature or significant bug fix (skip for refactors, minor fixes, and internal changes).
  • Developer documentation under docs/ updated if behavior or architecture changed.

4. Pull request

  • PR body built from .github/PULL_REQUEST_TEMPLATE.md, every section filled.
  • All checklist items kept and marked [x], [ ], or [/].
  • All HTML comments removed from the PR body.
  • PR created with gh pr create --body-file <file> (not --body).
  • If CHANGELOG.md used a TODO placeholder (no issue confidently identified yet — an existing issue link always stays), it was replaced with the real PR-number link after PR creation, then committed and pushed. If an issue is identified or created later, the link is switched to the issue.

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • [/] I added screenshots in the PR description (if change is visible to the user)
  • [/] I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • [/] I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

@Siedlerchr
Siedlerchr marked this pull request as ready for review July 19, 2026 22:03
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Keep entry previews responsive when switching large libraries

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Make preview rendering atomic and ignore stale background results during library switches.
• Build automatic group trees with temporary indexes for faster merges in large libraries.
• Load group counts lazily when visible, keeping preview updates responsive.
Diagram

graph TD
U(["User"]) --> A["Activate library"]
A --> G["Auto group paths"] --> T["TreeCollector index"] --> V["Group tree UI"]
V -->|"visible group counts"| X["TaskExecutor"]
A --> P["PreviewViewer"] --> X --> R["RichHtmlView"]

subgraph Legend
direction LR
_u(["User"]) ~~~ _ui["UI component"] ~~~ _bg(("Background task"))
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Cache automatic group trees across activations
  • ➕ Near-instant re-activation for unchanged libraries
  • ➕ Avoids recomputing group-path merges repeatedly
  • ➖ Hard invalidation: must track entry, group config, and metadata changes
  • ➖ Increases retained memory footprint for open libraries
2. Use dedicated executor or priority scheduling for previews
  • ➕ Hard guarantees that previews preempt group-count/group rebuild work
  • ➕ Simplifies reasoning about UI responsiveness under load
  • ➖ Threading/config complexity (pool sizing, shutdown, lifecycle)
  • ➖ Risk of subtle deadlocks or starvation if misconfigured
3. Actively cancel in-flight preview/group tasks
  • ➕ Reduces wasted CPU when users switch rapidly
  • ➕ Can further improve responsiveness under contention
  • ➖ Requires cooperative cancellation through preview generation and downstream libraries
  • ➖ More moving parts than sequence-based staleness checks

Recommendation: The chosen approach is well-balanced: (1) indexed merging improves group-tree rebuild scalability without retained caches, (2) lazy group-count evaluation avoids unnecessary work, and (3) sequence-guarded preview rendering prevents stale background results from overriding the latest selection. Consider executor prioritization or cooperative cancellation only if profiling still shows preview starvation after these changes.

Files changed (11) +258 / -37

Enhancement (4) +106 / -18
GroupNodeViewModel.javaLazy-load group hit counts and coalesce background recomputation +48/-15

Lazy-load group hit counts and coalesce background recomputation

• Introduces on-demand initialization for matched-entry computation used by group hit counts. Adds in-progress/pending flags to coalesce repeated updates and schedules count computation via the task executor, with debug logging on failures.

jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java

GroupTreeView.javaTrigger group count loading only when the count cell is shown +1/-0

Trigger group count loading only when the count cell is shown

• Ensures matched-entry computation for hit counts starts only when group count display is enabled and the number cell is created/bound (i.e., becomes visible).

jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java

AutomaticGroup.javaUse indexed tree merge API for automatic subgroup construction +3/-3

Use indexed tree merge API for automatic subgroup construction

• Switches automatic subgroup construction to the new TreeCollector.mergeIntoTree(stream, keyExtractor) overload, keying by group identity/equality via GroupTreeNode::getGroup.

jablib/src/main/java/org/jabref/model/groups/AutomaticGroup.java

TreeCollector.javaAdd indexed mergeIntoTree overload with temporary hash indexes +54/-0

Add indexed mergeIntoTree overload with temporary hash indexes

• Introduces a stream-based merge method that uses a root index and per-parent child indexes (IdentityHashMap) to find equivalent siblings in expected constant time during merges. Keeps existing collector-based behavior intact while providing a faster path for large automatic-group merges.

jablib/src/main/java/org/jabref/model/util/TreeCollector.java

Bug fix (1) +36 / -18
PreviewViewer.javaMake preview updates atomic and discard stale background results +36/-18

Make preview updates atomic and discard stale background results

• Adds an update sequence to prevent older background preview results (and cover refresh) from overwriting newer selections during rapid switching. Renders HTML with current base-URI options in a single JavaFX-thread update and refactors highlighting into a pure function returning highlighted HTML.

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java

Tests (2) +47 / -0
GroupNodeViewModelTest.javaUpdate tests to explicitly initialize lazy hit counts +2/-0

Update tests to explicitly initialize lazy hit counts

• Adjusts automatic-group hit-count tests to call ensureMatchedEntriesLoaded before asserting sizes, matching the new lazy evaluation behavior.

jabgui/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java

TreeCollectorTest.javaAdd regression test for merging children under equivalent roots +45/-0

Add regression test for merging children under equivalent roots

• Adds a unit test verifying that two equivalent roots are merged into one and their children are combined when using the new indexed mergeIntoTree overload.

jablib/src/test/java/org/jabref/model/util/TreeCollectorTest.java

Documentation (3) +68 / -0
CHANGELOG.mdAdd changelog entry for large-library switching responsiveness +1/-0

Add changelog entry for large-library switching responsiveness

• Documents the user-visible improvement: previews remain responsive while automatic groups and counts refresh during large library switches.

CHANGELOG.md

0066-index-automatic-group-tree-merges.mdAdd ADR for indexed automatic-group tree merging +60/-0

Add ADR for indexed automatic-group tree merging

• Introduces a decision record explaining why temporary hash indexes are used during merge to avoid repeated sibling scans, including options considered and consequences.

docs/decisions/0066-index-automatic-group-tree-merges.md

ux.mdAdd UX requirement for preview responsiveness on library activation +7/-0

Add UX requirement for preview responsiveness on library activation

• Adds a requirement that automatic group construction and group-count evaluation must not delay preview rendering when activating large libraries.

docs/requirements/ux.md

Other (1) +1 / -1
build.gradle.ktsBump html-to-node to 0.2.0-SNAPSHOT +1/-1

Bump html-to-node to 0.2.0-SNAPSHOT

• Updates the constrained dependency version to a snapshot required by the preview rendering changes pending upstream release.

versions/build.gradle.kts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Requirement heading level ## ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The newly added OpenFastTrace requirement uses a ## heading, but the compliance rule specifies
requirement entries must use a ###  heading format. This may break consistency with the mandated
OpenFastTrace formatting expectations.
Code

docs/requirements/ux.md[R43-44]

+## Activating large libraries keeps entry previews responsive
+`req~ux.active-library.preview-responsiveness~1`
Evidence
PR Compliance ID 23 requires requirement entries to use a ###  heading. The added requirement
entry at docs/requirements/ux.md[43-44] uses ## ... instead.

AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format): AGENTS.md: OpenFastTrace: Add Requirement Entries for New Features or Significant Bug Fixes (Correct Format)
docs/requirements/ux.md[43-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new OpenFastTrace requirement entry uses a `##` heading, but the compliance checklist mandates a `### <title>` heading for requirement entries.
## Issue Context
This PR added a new requirement entry `req~ux.active-library.preview-responsiveness~1`. To comply with the documented OFT formatting rule, adjust the heading level (or otherwise align the entry format to the required template).
## Fix Focus Areas
- docs/requirements/ux.md[43-49]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stale group counts ✓ Resolved 🐞 Bug ≡ Correctness
Description
GroupNodeViewModel.ensureMatchedEntriesLoaded() suppresses recomputation once
matchedEntriesInitialized is true, but group refreshes do not invalidate that flag when counts are
disabled (updateMatchedEntries() returns early), so re-enabling counts can bind to stale hit values.
This yields incorrect group counts after toggling count display off, changing group
definitions/matches, then toggling it back on.
Code

jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java[R214-218]

+    void ensureMatchedEntriesLoaded() {
+        if (!matchedEntriesInitialized) {
+            updateMatchedEntries();
+        }
+    }
Evidence
The lazy-load path only recomputes when matchedEntriesInitialized is false, but nothing clears
that flag on group refresh when counts are disabled (because updateMatchedEntries() returns
early). When the preference is enabled again, the UI binds hits and only calls
ensureMatchedEntriesLoaded(), which will not recompute if the flag remained true, leaving stale
counts visible.

jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java[214-218]
jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java[311-358]
jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java[365-389]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Group hit counts can become stale across the sequence: counts enabled (cache initialized) → counts disabled → group definition/matching changes → counts re-enabled. The cache (`matchedEntriesInitialized` + `matchedEntries`) is not invalidated in that scenario, so `ensureMatchedEntriesLoaded()` won’t trigger a rebuild.
### Issue Context
- `ensureMatchedEntriesLoaded()` only calls `updateMatchedEntries()` when `matchedEntriesInitialized` is false.
- `updateMatchedEntries()` returns immediately when `shouldDisplayGroupCount` is false, leaving the cache/flag unchanged.
- `GroupTreeView` calls `ensureMatchedEntriesLoaded()` when counts are enabled, so stale caches persist when re-enabling.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java[214-218]
- jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java[311-358]
- jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java[365-389]
### Suggested fix
1. Invalidate the cache on any group refresh/invalidation, even if counts are currently disabled (e.g., set `matchedEntriesInitialized = false`; optionally clear `matchedEntries` to avoid briefly showing stale sizes when rebinding).
2. Ensure that when counts are re-enabled, a recomputation is forced if the cache was invalidated while counts were off.
3. Add/adjust a test that covers: initialize counts → disable counts → change group definition or trigger `refreshGroup()` → re-enable counts → verify recomputation occurs (not stale values).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread docs/requirements/ux.md Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java
@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Your pull request conflicts with the target branch.

Please merge with your code. For a step-by-step guide to resolve merge conflicts, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line.

koppor and others added 4 commits July 20, 2026 14:06
- Avoid a redundant full-database rescan when number cells re-render
  while the initial group-count load is still in flight
- Log group-count update failures at warn instead of debug
- Document why group-count tasks go through schedule() (separate pool,
  keeps the worker pool free for preview rendering)
- Force a group-count reload when the display-group-count preference is
  re-enabled after a skipped recompute
- Confine PreviewViewer.update() to the FX thread so the unsynchronized
  updateSequence guard is safe when entry observables fire from
  background threads
- Drop the now-unused Collector implementation from TreeCollector and
  the now-unused GroupTreeNode.isSameGroupAs
- Cover the recursive grandchild-collision path in TreeCollectorTest
- Move the CHANGELOG entry from Fixed to Changed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW1qh78rm2gy42vtixbmM4
The OFT requirement template (docs/requirements/index.md, AGENTS.md)
specifies a "### <title>" heading; the file's older entries predate it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW1qh78rm2gy42vtixbmM4
The skipped recompute already invalidated the cache; also clear the
matched-entries set so rebinding the number cell never briefly shows the
outdated count, and cover the disable -> refresh -> re-enable sequence
with a test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW1qh78rm2gy42vtixbmM4
Comment thread jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java Outdated
@Siedlerchr Siedlerchr added status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers dev: performance and removed status: changes-required Pull requests that are not yet complete labels Jul 20, 2026
@subhramit

Copy link
Copy Markdown
Member

Improves #15962

@Siedlerchr
Siedlerchr enabled auto-merge July 20, 2026 19:58

@koppor koppor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure what's happening here, but we need to test "in the wild"; thus approving merge to main.

@Siedlerchr
Siedlerchr added this pull request to the merge queue Jul 21, 2026
@github-actions github-actions Bot added the status: to-be-merged PRs which are accepted and should go into the merge-queue. label Jul 21, 2026
Merged via the queue into main with commit 772fe14 Jul 21, 2026
100 of 106 checks passed
@Siedlerchr
Siedlerchr deleted the agent/improve-large-library-switching branch July 21, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev: performance status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers status: to-be-merged PRs which are accepted and should go into the merge-queue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants