Skip to content

Commit

Permalink
Remove ConcurrentLinkedHashMap to work in GraalVM native-image
Browse files Browse the repository at this point in the history
  • Loading branch information
brunchboy committed Jan 28, 2024
1 parent 79a7b68 commit a906660
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 24 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ This change log follows the conventions of

## [Unreleased][unreleased]

Nothing so far.
- Replaced the dependency on
[ConcurrentLinkedHashMap](https://github.com/ben-manes/concurrentlinkedhashmap)
with a much-simplified in-project implementation based on some suggestions kindly
shared by [Ben Manes](https://github.com/ben-manes), the author of that library,
so that Beat Link Trigger can work as a pre-compiled GraalVM
[native-image](https://www.graalvm.org/latest/reference-manual/native-image/), while
retaining its backwards compatibility with Java 6 environments (like afterglow-max).
Thanks to [Noah Zoschke](https://github.com/nzoschke) for the pull request that led
to this change in direction.

## [7.3.0] - 2023-11-24

Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ binding to the logging framework you would like to use.
![Create library jar](https://github.com/Deep-Symmetry/beat-link/workflows/Create%20library%20jar/badge.svg)

You will also need
[ConcurrentLinkedHashMap](https://github.com/ben-manes/concurrentlinkedhashmap)
for maintaining album art caches, and
[Remote Tea](https://sourceforge.net/projects/remotetea/), so Maven
[Remote Tea](https://sourceforge.net/projects/remotetea/) for creating and parsing old-version NFS packets, so Maven
is by far your easiest bet, because it will take care of _all_ these
libraries for you.

Expand Down
7 changes: 1 addition & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.deepsymmetry</groupId>
<artifactId>beat-link</artifactId>
<version>7.3.0</version>
<version>7.4.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>beat-link</name>
Expand Down Expand Up @@ -94,11 +94,6 @@
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
<artifactId>concurrentlinkedhashmap-lru</artifactId>
<version>1.4.2</version>
</dependency>
</dependencies>

<build>
Expand Down
105 changes: 91 additions & 14 deletions src/main/java/org/deepsymmetry/beatlink/data/ArtFinder.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.deepsymmetry.beatlink.data;

import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import org.deepsymmetry.beatlink.*;
import org.deepsymmetry.beatlink.dbserver.*;
import org.slf4j.Logger;
Expand All @@ -12,6 +11,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
* <p>Watches for new metadata to become available for tracks loaded on players, and queries the
Expand Down Expand Up @@ -59,6 +59,17 @@ public void metadataChanged(TrackMetadataUpdate update) {
}
};

/**
* Removes the specified art from our second-level cache if it was present there.
*
* @param artReference identifies the album art which should no longer be cached.
*/
private synchronized void removeArtFromCache(DataReference artReference) {
artCache.remove(artReference);
artCacheRecentlyUsed.remove(artReference);
artCacheEvictionQueue.remove(artReference);
}

/**
* Our mount listener evicts any cached artwork that belong to media databases which have been unmounted, since
* they are no longer valid.
Expand All @@ -76,7 +87,7 @@ public void mediaUnmounted(SlotReference slot) {
for (DataReference artReference : keys) {
if (SlotReference.getSlotReference(artReference) == slot) {
logger.debug("Evicting cached artwork in response to unmount report {}", artReference);
artCache.remove(artReference);
removeArtFromCache(artReference);
}
}
// Again iterate over a copy to avoid concurrent modification issues.
Expand Down Expand Up @@ -163,7 +174,7 @@ private void clearArt(DeviceAnnouncement announcement) {
// Again iterate over a copy to avoid concurrent modification issues
for (DataReference art : new HashSet<DataReference>(artCache.keySet())) {
if (art.player == player) {
artCache.remove(art);
removeArtFromCache(art);
}
}
}
Expand Down Expand Up @@ -234,10 +245,32 @@ public AlbumArt getLatestArtFor(DeviceUpdate update) {
/**
* Establish the second-level artwork cache. Since multiple tracks share the same art, it can be worthwhile to keep
* art around even for tracks that are not currently loaded, to save on having to request it again when another
* track from the same album is loaded.
* track from the same album is loaded. This along with {@link #artCacheEvictionQueue} and
* {@link #artCacheRecentlyUsed} provide a simple implementation of the “clock” or “second-chance” variant on a
* least-recently-used cache. Thanks to Ben Manes for suggesting this approach as a replacement for the deprecated
* <a href="https://github.com/ben-manes/concurrentlinkedhashmap">ConcurrentLinkedHashMap</a> which was preventing
* Beat Link from working in GraalVM
* <a href="https://www.graalvm.org/latest/reference-manual/native-image/">native-image</a> environments, without
* forcing us to abandon Java 6 compatibility which is still useful for afterglow-max.
*/
private final ConcurrentHashMap<DataReference, AlbumArt> artCache = new ConcurrentHashMap<DataReference, AlbumArt>();

/**
* Keeps track of the order in which art has been added to the cache, so older and unused art is prioritized for
* eviction.
*/
private final LinkedList<DataReference> artCacheEvictionQueue = new LinkedList<DataReference>();

/**
* Keeps track of whether artwork has been used since it was added to the cache or previously considered for
* eviction, so older and unused art is prioritized for eviction.
*/
private final HashSet<DataReference> artCacheRecentlyUsed = new HashSet<DataReference>();

/**
* Establishes how many album art images we retain in our second-level cache.
*/
private final ConcurrentLinkedHashMap<DataReference, AlbumArt> artCache =
new ConcurrentLinkedHashMap.Builder<DataReference, AlbumArt>().maximumWeightedCapacity(DEFAULT_ART_CACHE_SIZE).build();
private final AtomicInteger artCacheSize = new AtomicInteger(DEFAULT_ART_CACHE_SIZE);

/**
* Check how many album art images can be kept in the in-memory second-level cache.
Expand All @@ -246,7 +279,28 @@ public AlbumArt getLatestArtFor(DeviceUpdate update) {
* in-memory art cache.
*/
public long getArtCacheSize() {
return artCache.capacity();
return artCacheSize.get();
}

/**
* Removes an element from the second-level artwork cache. Looks for the first item in the eviction queue that
* has not been used since it was created or considered for eviction and removes that. Any items which are found
* to have been used are instead moved to the end of the queue and marked unused. Must be called by one of the
* synchronized methods that deal with manipulating the cache.
*/
private void evictFromArtCache() {
boolean evicted = false;
while (!evicted && !artCacheEvictionQueue.isEmpty()) {
DataReference candidate = artCacheEvictionQueue.removeFirst();
if (artCacheRecentlyUsed.remove(candidate)) {
// This artwork has been used, give it a second chance.
artCacheEvictionQueue.addLast(candidate);
} else {
// This candidate is ready to be evicted.
artCache.remove(candidate);
evicted = true;
}
}
}

/**
Expand All @@ -258,18 +312,21 @@ public long getArtCacheSize() {
*
* @throws IllegalArgumentException if {@code} size is less than 1
*/
public void setArtCacheSize(int size) {
public synchronized void setArtCacheSize(int size) {
if (size < 1) {
throw new IllegalArgumentException("size must be at least 1");

}
artCache.setCapacity(size);
artCacheSize.set(size);
while (artCache.size() > size) {
evictFromArtCache();
}
}

/**
* Controls whether we attempt to obtain high-resolution art when it is available.
*/
private AtomicBoolean requestHighResolutionArt = new AtomicBoolean(true);
private final AtomicBoolean requestHighResolutionArt = new AtomicBoolean(true);

/**
* Check whether we are requesting high-resolution artwork when it is available.
Expand All @@ -289,6 +346,17 @@ public void setRequestHighResolutionArt(boolean shouldRequest) {
requestHighResolutionArt.set(shouldRequest);
}

/**
* Adds artwork to our second-level cache, evicting older unused art if necessary to enforce the size limit.
*/
private synchronized void addArtToCache(DataReference artReference, AlbumArt art) {
while (artCache.size() >= artCacheSize.get()) {
evictFromArtCache();
}
artCache.put(artReference, art);
artCacheEvictionQueue.addLast(artReference);
}

/**
* Ask the specified player for the album art in the specified slot with the specified rekordbox ID,
* using downloaded media instead if it is available, and possibly giving up if we are in passive mode.
Expand All @@ -308,6 +376,7 @@ private AlbumArt requestArtworkInternal(final DataReference artReference, final
if (sourceDetails != null) {
final AlbumArt provided = MetadataFinder.getInstance().allMetadataProviders.getAlbumArt(sourceDetails, artReference);
if (provided != null) {
addArtToCache(artReference, provided);
return provided;
}
}
Expand All @@ -329,7 +398,7 @@ public AlbumArt useClient(Client client) throws Exception {
try {
AlbumArt artwork = ConnectionManager.getInstance().invokeWithClientSession(artReference.player, task, "requesting artwork");
if (artwork != null) { // Our file load or network request succeeded, so add to the level 2 cache.
artCache.put(artReference, artwork);
addArtToCache(artReference, artwork);
}
return artwork;
} catch (Exception e) {
Expand Down Expand Up @@ -412,7 +481,13 @@ private AlbumArt findArtInMemoryCaches(DataReference artReference) {
}

// Not in the hot cache, see if it is in our LRU cache
return artCache.get(artReference);
synchronized (this) {
AlbumArt found = artCache.get(artReference);
if (found != null) {
artCacheRecentlyUsed.add(artReference);
}
return found;
}
}

/**
Expand All @@ -429,9 +504,9 @@ private AlbumArt findArtInMemoryCaches(DataReference artReference) {
* <p>To reduce latency, updates are delivered to listeners directly on the thread that is receiving packets
* from the network, so if you want to interact with user interface objects in listener methods, you need to use
* <code><a href="http://docs.oracle.com/javase/8/docs/api/javax/swing/SwingUtilities.html#invokeLater-java.lang.Runnable-">javax.swing.SwingUtilities.invokeLater(Runnable)</a></code>
* to do so on the Event Dispatch Thread.
* to do so on the Event Dispatch Thread.</p>
*
* Even if you are not interacting with user interface objects, any code in the listener method
* <p>Even if you are not interacting with user interface objects, any code in the listener method
* <em>must</em> finish quickly, or it will add latency for other listeners, and updates will back up.
* If you want to perform lengthy processing of any sort, do so on another thread.</p>
*
Expand Down Expand Up @@ -617,6 +692,8 @@ public void run() {
});
hotCache.clear();
artCache.clear();
artCacheRecentlyUsed.clear();
artCacheEvictionQueue.clear();
deliverLifecycleAnnouncement(logger, false);
}
}
Expand Down

0 comments on commit a906660

Please sign in to comment.