Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Dbsync performance improvements #8496

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

WIP: Dbsync performance improvements #8496

wants to merge 26 commits into from

Conversation

Siedlerchr
Copy link
Member

@Siedlerchr Siedlerchr commented Feb 9, 2022

I could not push to the original one
Follow up to #8494

Introduced a dummy FXThread method for the CurrentTaskExecutor to fix the tests and added some synchronized to prevent errors with transactions. Need to check if we need this for other udpates as well

  • Change in CHANGELOG.md described in a way that is understandable for the average user (if applicable)
  • Tests created for changes (if applicable)
  • Manually tested changed features in running JabRef (always required)
  • Screenshots added in PR description (for UI changes)
  • Checked documentation: Is the information available and up to date? If not, I created an issue at https://github.com/JabRef/user-documentation/issues or, even better, I submitted a pull request to the documentation repository.

@Siedlerchr Siedlerchr changed the title Dbsync Dbsync performance improvements Feb 9, 2022
@Siedlerchr Siedlerchr added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 9, 2022
@@ -221,7 +231,8 @@ public void synchronizeLocalDatabase() {

if (!entriesToInsertIntoLocalDatabase.isEmpty()) {
// in case entries should be added into the local database, insert them
bibDatabase.insertEntries(dbmsProcessor.getSharedEntries(entriesToInsertIntoLocalDatabase), EntriesEventSource.SHARED);
taskExecutor.runInFXThread(() ->
Copy link
Member

Choose a reason for hiding this comment

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

Is this really necessary? I would say it's the job of the ui to make sure it handles notifications about new entries on the right thread, and not of the logic. In fact, the logic shouldn't even know about the concept of fx-threads.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, while I agree with you that from an architectural standpoint this looks odd, the key problem is that if you insert anything in the bib database or modify a field, the GUI observables in the entry editor or maintable are called as well because they are bound to the bib database object.

Copy link
Member

Choose a reason for hiding this comment

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

For this situation, we have the "forUi" helper

/**
* Returns a wrapper around the given list that posts changes on the JavaFX thread.
*/
public static <T> ObservableList<T> forUI(ObservableList<T> list) {
return new UiThreadList<>(list);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but I have encountered this before, this leads to a deadlock under certain conditions:
Comment that line out and connect to the shared SQL library in #8485
See here. I can only solve this by adding a timeout in the UI Thread list

grafik

Copy link
Member

Choose a reason for hiding this comment

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

NOTE: applications should avoid flooding JavaFX with too many pending Runnables. Otherwise, the application may become unresponsive. Applications are encouraged to batch up multiple operations into fewer runLater calls. Additionally, long-running operations should be done on a background thread where possible, freeing up the JavaFX Application Thread for GUI operations.

That might be the reason. How many source changes does the database trigger?

Copy link
Member

Choose a reason for hiding this comment

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

Nice analysis, thanks!

What I don't understand is why does the background thread pauses in the UiThreadList? Shouldn't it go on, sync the rest of the changes and finally give the synced entry list free?

Maybe it works if one adds the forUi call at the last position here

entriesSorted = new SortedList<>(entriesFiltered);
(or for the entriesFiltered, if otherwise the search no longer works). Then all the syncs / updates of the mapped database would still run in the background thread.

Copy link
Member Author

Choose a reason for hiding this comment

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

the BackgroundThread only indirectly pauses in the UIList because

  1. When calling add() on the SynchronizedLIst, Background Thread acquires a mutex for the whole list.
  2. it's waiting for the CountDownLatch that never happens; this is why I tested to solve it with a timeout in the latch.
     CountDownLatch latch = new CountDownLatch(1);
            Platform.runLater(() -> {
                fireChange(change);
                latch.countDown(); //never gets called, because we are stil in fireChange  and bocked
            });

            try {
                latch.await();
               // boolean latched = latch.await(3L, TimeUnit.SECONDS);
               // LOGGER.debug("Result from latch {}", latched); // if this is false, it was canceled by the timeout

            } catch (InterruptedException e) {
                LOGGER.error("Error while running on JavaFX thread", e);
            } finally {
                latch.countDown();
            }

Copy link
Member Author

Choose a reason for hiding this comment

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

@tobiasdiez I could solve it for the table, but I discovered that this also affects the GroupNodeViewModel, where a listener is registered. this can happend when switching lbraries now.
May also need to adapt it for the FilteredList...

entriesList = databaseContext.getDatabase().getEntries();
entriesList.addListener(this::onDatabaseChanged);

Copy link
Member

Choose a reason for hiding this comment

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

Mhh, maybe it's a good idea to step back and try to come up with a general solution to these problems?
Off the top of my head, we have two ways to handle background threads that result in updates to the UI:

  • Background threads are only allowed to calculate things, but never change any of the data model classes directly.
  • Background threads are allowed to change data models, but at every point where the data model classes are bind to the UI we make sure that the updates are happening on the JavaFX thread (that essentially only concerns the main table, group view and entry editor).

I think both ways have advantages and disadvantages. I don't have a strong preference right now, do you? But I do think it would make sense to decide which approach we take and then do changes accordingly.

Copy link
Member Author

Choose a reason for hiding this comment

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

From an architectural standpoint, updating the model should not have direct influences of the UI. The UI should take care that it is updating things in the right thread. This is more or less our current architecture. (2nd approach).

The key problem is more that we have SynchronizedList which is bound to the UI.
It has a mutex on all methods which will be held when elements are inserted. So while Elements are inserted a lock is held. Firing Changes on the FX Thread will always access the underlying list.
I will see if this work with the forUI for groups as well

* upstream/main:
  Update Gradle Wrapper from 7.3.3 to 7.4. (#8499)
  Bump flexmark-ext-gfm-strikethrough from 0.62.2 to 0.64.0 (#8500)
  Bump tika-core from 2.2.1 to 2.3.0 (#8501)
  Bump classgraph from 4.8.138 to 4.8.139 (#8502)
  Fix for opening console on active database (#8492)
@Siedlerchr Siedlerchr removed the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Feb 18, 2022
@Siedlerchr
Copy link
Member Author

This need some more thinking how to solve blocking threads when the EventBus fires. Therefore I close this for the moment and reopen when I have more insights

* upstream/main: (387 commits)
  Show a warning in the merge dialog when authors are the same but formatted differently (#9088)
  Fix subdatabase from aux on cli (#9117)
  Visual improvements to LinkedFilesEditor (#9114)
  SLR Remove "last-search-date" (#9116)
  Hide diffs when one of the field values is blank a.k.a no conflict (#9110)
  Squashed 'buildres/csl/csl-locales/' changes from e637746677..b2afeb4d87
  Squashed 'buildres/csl/csl-styles/' changes from c750b6e..8d69f16
  Fix title case capitalization after en-dash characters (#9102)
  Update journal abbrev list (#9109)
  Fix CSL rendering in case of article (#8607)
  [WIP][GSOC22] - C - Improve the external changes resolver dialog (#9021)
  Bump jsoup from 1.15.1 to 1.15.3 (#9103)
  Bump checkstyle from 10.3.2 to 10.3.3 (#9104)
  Bump postgresql from 42.4.2 to 42.5.0 (#9105)
  Bump unirest-java from 3.13.10 to 3.13.11 (#9106)
  Include check for TimeStamp (#9089)
  Close OO connection on JabRef exit (#9076)
  Bump slf4j-tinylog from 2.4.1 to 2.5.0 (#9085)
  Bump bcprov-jdk18on from 1.71 to 1.71.1 (#9079)
  Bump tinylog-impl from 2.4.1 to 2.5.0 (#9086)
  ...

# Conflicts:
#	src/main/java/org/jabref/gui/shared/SharedDatabaseUIManager.java
#	src/main/java/org/jabref/gui/util/DefaultTaskExecutor.java
#	src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java
@Siedlerchr Siedlerchr reopened this Sep 2, 2022
@Siedlerchr Siedlerchr changed the title Dbsync performance improvements WIP: Dbsync performance improvements Sep 2, 2022
Platform.runLater(()-> {
entriesList = databaseContext.getDatabase().getEntries();
entriesList.addListener(this::onDatabaseChanged);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck> reported by reviewdog 🐶
There is more than 1 empty line after this line.

fireChange(change);
latch.countDown();
try {
fireChange(change);
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck> reported by reviewdog 🐶
Blank line at end of block should be removed

try {
fireChange(change);

}
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.blocks.RightCurlyCheck> reported by reviewdog 🐶
'}' at column 17 should be on the same line as the next part of a multi-block statement (one that directly contains multiple blocks: if/else-if/else, do/while or try/catch/finally).


}
finally
{
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.blocks.LeftCurlyCheck> reported by reviewdog 🐶
'{' at column 17 should be on the previous line.

/**
* If {@link TaskExecutor} is an instance of {@link DefaultTaskExecutor} then it is equivalent of calling {@link DefaultTaskExecutor#runInJavaFXThread(Runnable)}
* otherwise it is executed on the same thread as the caller. This is extremely useful for testing code that does calls to the FXThread
* @param runnable The runnable to execute
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.javadoc.RequireEmptyLineBeforeBlockTagGroupCheck> reported by reviewdog 🐶
Javadoc tag '@param' should be preceded with an empty line.

* otherwise it is executed on the same thread as the caller. This is extremely useful for testing code that does calls to the FXThread
* @param runnable The runnable to execute
*/
void runInFXThread(Runnable runnable);
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck> reported by reviewdog 🐶
Blank line at end of block should be removed

if (idVersionEntry.getValue() > localEntry.getSharedBibEntryData().getVersion()) {
Optional<BibEntry> sharedEntry = dbmsProcessor.getSharedEntry(idVersionEntry.getKey());
if (sharedEntry.isPresent()) {
// update fields
localEntry.setType(sharedEntry.get().getType(), EntriesEventSource.SHARED);
localEntry.getSharedBibEntryData()
.setVersion(sharedEntry.get().getSharedBibEntryData().getVersion());


taskExecutor.runInFXThread(()-> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck> reported by reviewdog 🐶
There is more than 1 empty line after this line.

if (idVersionEntry.getValue() > localEntry.getSharedBibEntryData().getVersion()) {
Optional<BibEntry> sharedEntry = dbmsProcessor.getSharedEntry(idVersionEntry.getKey());
if (sharedEntry.isPresent()) {
// update fields
localEntry.setType(sharedEntry.get().getType(), EntriesEventSource.SHARED);
localEntry.getSharedBibEntryData()
.setVersion(sharedEntry.get().getSharedBibEntryData().getVersion());


taskExecutor.runInFXThread(()-> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck> reported by reviewdog 🐶
There is more than 1 empty line after this line.

if (!remoteEntryMatchingOneLocalEntryFound) {
entriesToInsertIntoLocalDatabase.add(idVersionEntry.getKey());
}
}

if (!entriesToInsertIntoLocalDatabase.isEmpty()) {

taskExecutor.runInFXThread( () -> bibDatabase.insertEntries(dbmsProcessor.getSharedEntries(entriesToInsertIntoLocalDatabase), EntriesEventSource.SHARED));
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck> reported by reviewdog 🐶
'(' is followed by whitespace.

if (!remoteEntryMatchingOneLocalEntryFound) {
entriesToInsertIntoLocalDatabase.add(idVersionEntry.getKey());
}
}

// TODO: Update at once? or wrap whole method again in taskExecutor?
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck> reported by reviewdog 🐶
There is more than 1 empty line after this line.

if (!entriesToInsertIntoLocalDatabase.isEmpty()) {

taskExecutor.runInFXThread( () -> bibDatabase.insertEntries(dbmsProcessor.partitionAndGetSharedEntries(entriesToInsertIntoLocalDatabase), EntriesEventSource.SHARED) );
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck> reported by reviewdog 🐶
'(' is followed by whitespace.

if (!entriesToInsertIntoLocalDatabase.isEmpty()) {

taskExecutor.runInFXThread( () -> bibDatabase.insertEntries(dbmsProcessor.partitionAndGetSharedEntries(entriesToInsertIntoLocalDatabase), EntriesEventSource.SHARED) );
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.ParenPadCheck> reported by reviewdog 🐶
')' is preceded with whitespace.

if (!entriesToInsertIntoLocalDatabase.isEmpty()) {

taskExecutor.runInFXThread( () -> bibDatabase.insertEntries(dbmsProcessor.partitionAndGetSharedEntries(entriesToInsertIntoLocalDatabase), EntriesEventSource.SHARED) );
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.SingleSpaceSeparatorCheck> reported by reviewdog 🐶
Use a single space to separate non-whitespace characters.

* upstream/main:
  Fix parsing of JabRef v5.7 study.yml files (#9124)
  Fix integrity check  for tilde accents in author names (#9097)
  Rework the Define study parameters dialog (#9123)
  Fix parsing of save actions (#9122)
  Fix exception that occurs when saving name formatters (#9121)
  Refine code for BibEntry#replaceDownloadedFile (#9118)
* upstream/main:
  fix l10n
  Compile fix

# Conflicts:
#	src/main/java/org/jabref/gui/slr/StudyDatabaseItem.java
fuuu
remoteEntryMatchingOneLocalEntryFound = true;
if (idVersionEntry.getValue() > localEntry.getSharedBibEntryData().getVersion()) {
Optional<BibEntry> sharedEntry = dbmsProcessor.getSharedEntry(idVersionEntry.getKey());
remoteEntryMatchingOneLocalEntryFound = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.whitespace.EmptyLineSeparatorCheck> reported by reviewdog 🐶
There is more than 1 empty line after this line.

remoteEntryMatchingOneLocalEntryFound = true;


if (idVersionEntry.getValue() > localEntry.getSharedBibEntryData().getVersion()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck> reported by reviewdog 🐶
Blank line at start of block should be removed

BackgroundTask.wrap(() -> dbmsProcessor.partitionAndGetSharedEntries(entriesToInsertIntoLocalDatabase))
.onSuccess(entries-> {
// in case entries should be added into the local database, insert them
bibDatabase.insertEntries(entries, EntriesEventSource.SHARED);
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck> reported by reviewdog 🐶
Blank line at end of block should be removed

// in case entries should be added into the local database, insert them
bibDatabase.insertEntries(entries, EntriesEventSource.SHARED);

}).executeWith(taskExecutor);
Copy link
Contributor

Choose a reason for hiding this comment

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

🚫 [reviewdog] <com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck> reported by reviewdog 🐶
Blank line at end of block should be removed

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.

None yet

3 participants