From 94bdb4a0b0f6fb511eaf772ac18547e8da10f161 Mon Sep 17 00:00:00 2001 From: Ondro Mihalyi Date: Sat, 4 Apr 2026 17:41:28 +0200 Subject: [PATCH] Batch status change notifications to avoid per-file event overhead Replace per-file PROP_FILE_STATUS_CHANGED firing in refreshStatusesBatch with a single PROP_FILES_STATUS_CHANGED batch event, eliminating 100k redundant propertyChange/schedule/SwingUtilities.invokeLater calls on first load. This improves performance a bit because it eliminates many method calls. However, in the end, the number of files changed is the same so the event handler still needs to process all of them. Also skip firing events for up-to-date files that are not yet in the cache, since UPTODATE is the default for managed files. This drastically reduces the time spent in the refreshStatusesBatch method on big repositories executed when Commit dialog opens. For example, on the Netbeans repository, from around 8 seconds to 20ms. --- .../org/netbeans/modules/git/FileStatusCache.java | 10 ++++++++-- ide/git/src/org/netbeans/modules/git/GitVCS.java | 9 +++++++++ .../git/ui/diff/MultiDiffPanelController.java | 15 ++++++++++++--- .../git/ui/status/VersioningPanelController.java | 8 ++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/ide/git/src/org/netbeans/modules/git/FileStatusCache.java b/ide/git/src/org/netbeans/modules/git/FileStatusCache.java index 011f0983bd2e..c333fccbc4c0 100644 --- a/ide/git/src/org/netbeans/modules/git/FileStatusCache.java +++ b/ide/git/src/org/netbeans/modules/git/FileStatusCache.java @@ -57,6 +57,7 @@ public class FileStatusCache { public static final String PROP_FILE_STATUS_CHANGED = "status.changed"; // NOI18N + public static final String PROP_FILES_STATUS_CHANGED = "status.changed.batch"; // NOI18N private final CacheIndex conflictedFiles, modifiedFiles, ignoredFiles; private static final Logger LOG = Logger.getLogger("org.netbeans.modules.git.status.cache"); //NOI18N @@ -562,6 +563,7 @@ private void refreshStatusesBatch (Map interestingFiles) { boolean fireEvent = true; current = getInfo(file); fi = checkForIgnore(fi, current, file); + boolean upToDateOnFirstLoad = current == null && fi.getStatus().equals(EnumSet.of(Status.UPTODATE)); if (equivalent(fi, current)) { // no need to fire an event if (Utilities.isWindows() || Utilities.isMac()) { @@ -570,6 +572,10 @@ private void refreshStatusesBatch (Map interestingFiles) { } else { continue; } + } else if (upToDateOnFirstLoad) { + // file is up-to-date and not yet in cache: UPTODATE is the implicit default, + // no cache update or event needed + continue; } boolean addToIndex = updateCachedValue(fi, file); indexUpdates.add(new IndexUpdateItem(file, fi, addToIndex)); @@ -579,8 +585,8 @@ private void refreshStatusesBatch (Map interestingFiles) { } updateIndexBatch(indexUpdates); } - for (ChangedEvent event : events) { - fireFileStatusChanged(event); + if (!events.isEmpty()) { + listenerSupport.firePropertyChange(PROP_FILES_STATUS_CHANGED, null, events); } } diff --git a/ide/git/src/org/netbeans/modules/git/GitVCS.java b/ide/git/src/org/netbeans/modules/git/GitVCS.java index c8951d42c32a..f138be32f9f3 100644 --- a/ide/git/src/org/netbeans/modules/git/GitVCS.java +++ b/ide/git/src/org/netbeans/modules/git/GitVCS.java @@ -23,6 +23,8 @@ import java.beans.PropertyChangeListener; import java.util.MissingResourceException; import java.io.File; +import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.logging.Logger; import java.util.prefs.PreferenceChangeEvent; @@ -111,6 +113,13 @@ public void propertyChange(PropertyChangeEvent event) { if (event.getPropertyName().equals(FileStatusCache.PROP_FILE_STATUS_CHANGED)) { FileStatusCache.ChangedEvent changedEvent = (FileStatusCache.ChangedEvent) event.getNewValue(); fireStatusChanged(changedEvent.getFile()); + } else if (event.getPropertyName().equals(FileStatusCache.PROP_FILES_STATUS_CHANGED)) { + List changedEvents = (List) event.getNewValue(); + Set files = new HashSet<>(changedEvents.size()); + for (FileStatusCache.ChangedEvent e : changedEvents) { + files.add(e.getFile()); + } + fireStatusChanged(files); } else if (event.getPropertyName().equals(Git.PROP_ANNOTATIONS_CHANGED)) { fireAnnotationsChanged((Set) event.getNewValue()); } else if (event.getPropertyName().equals(Git.PROP_VERSIONED_FILES_CHANGED)) { diff --git a/ide/git/src/org/netbeans/modules/git/ui/diff/MultiDiffPanelController.java b/ide/git/src/org/netbeans/modules/git/ui/diff/MultiDiffPanelController.java index 6ff1626e0f41..1c6caf7593bb 100644 --- a/ide/git/src/org/netbeans/modules/git/ui/diff/MultiDiffPanelController.java +++ b/ide/git/src/org/netbeans/modules/git/ui/diff/MultiDiffPanelController.java @@ -1123,15 +1123,24 @@ public void propertyChange (PropertyChangeEvent evt) { if (FileStatusCache.PROP_FILE_STATUS_CHANGED.equals(evt.getPropertyName())) { FileStatusCache.ChangedEvent changedEvent = (FileStatusCache.ChangedEvent) evt.getNewValue(); if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "File status for file {0} changed from {1} to {2}", new Object[] { - changedEvent.getFile(), + LOG.log(Level.FINE, "File status for file {0} changed from {1} to {2}", new Object[] { + changedEvent.getFile(), changedEvent.getOldInfo(), changedEvent.getNewInfo() } ); } - if (revisionLeft == Revision.HEAD // remove when we're able to refresh single file changes for Local vs. any revision + if (revisionLeft == Revision.HEAD // remove when we're able to refresh single file changes for Local vs. any revision && revisionRight == Revision.LOCAL && affectsView(changedEvent)) { applyChange(changedEvent); } + } else if (FileStatusCache.PROP_FILES_STATUS_CHANGED.equals(evt.getPropertyName())) { + @SuppressWarnings("unchecked") + List changedEvents = (List) evt.getNewValue(); + for (FileStatusCache.ChangedEvent changedEvent : changedEvents) { + if (revisionLeft == Revision.HEAD // remove when we're able to refresh single file changes for Local vs. any revision + && revisionRight == Revision.LOCAL && affectsView(changedEvent)) { + applyChange(changedEvent); + } + } } else if (DiffController.PROP_DIFFERENCES.equals(evt.getPropertyName())) { // something has changed Setup setup = currentSetup; diff --git a/ide/git/src/org/netbeans/modules/git/ui/status/VersioningPanelController.java b/ide/git/src/org/netbeans/modules/git/ui/status/VersioningPanelController.java index 2a0ed8d38079..5fd2266740c5 100644 --- a/ide/git/src/org/netbeans/modules/git/ui/status/VersioningPanelController.java +++ b/ide/git/src/org/netbeans/modules/git/ui/status/VersioningPanelController.java @@ -408,6 +408,14 @@ public void propertyChange (PropertyChangeEvent evt) { if (affectsView((FileStatusCache.ChangedEvent) evt.getNewValue())) { applyChange(changedEvent); } + } else if (FileStatusCache.PROP_FILES_STATUS_CHANGED.equals(evt.getPropertyName())) { + @SuppressWarnings("unchecked") + List changedEvents = (List) evt.getNewValue(); + for (FileStatusCache.ChangedEvent changedEvent : changedEvents) { + if (affectsView(changedEvent)) { + applyChange(changedEvent); + } + } } }