Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f61443b
Simple proof of concept for caching Fragments.
magicus Oct 25, 2019
0b8e939
Add -seed command line argument to start Amidst with a particular wor…
magicus Oct 20, 2019
156738a
Also add -world-type to set the initial world type for the initial seed.
magicus Oct 25, 2019
a1578c5
* Rename recycleFragment to retireFragment.
magicus Oct 27, 2019
764334e
Merge branch 'master' into magicus-fragment-caching
magicus Oct 27, 2019
75ab02a
* Add conditions for cache handling
magicus Oct 27, 2019
4f9ef92
Add cache invalidation. Add CalledOnlyBy annotation. Minor cleanup.
magicus Oct 29, 2019
53717ec
A hashmap of SoftRefs are more suitable to our purposes.
magicus Oct 29, 2019
70ecfcd
We need to check not only if the softref is in the cache, but also th…
magicus Oct 29, 2019
13b6a0a
Merge branch 'master' into magicus-fragment-caching-updated
burgerindividual Aug 25, 2020
e39b8e9
first commit of fragment changes
burgerindividual Aug 30, 2020
0f24afd
Merge branch 'master' into magicus-fragment-caching-updated
burgerindividual Sep 4, 2020
0618d39
Merge branch 'redo-fragment-multithreading' into multithreaded-fragme…
burgerindividual Sep 4, 2020
41c8b21
make the cache better, make recycling LIFO instead of FIFO, dont draw…
burgerindividual Sep 5, 2020
fe390d0
don't clean up on every change to the map
burgerindividual Sep 5, 2020
dc9b348
make the cache clean on a separate thread
burgerindividual Sep 6, 2020
f1bcd0a
make some cleaning stuff better
burgerindividual Sep 7, 2020
bbf3adf
get graph iterator from the EDT
burgerindividual Sep 7, 2020
826df14
dont do reloadAll on first load
burgerindividual Sep 7, 2020
dff78f0
invalidate instead of clear
burgerindividual Sep 8, 2020
4968452
isLoaded check turned out to be useless
burgerindividual Sep 8, 2020
bad342c
Merge branch 'redo-fragment-multithreading' into multithreaded-fragme…
burgerindividual Sep 15, 2020
23e9465
Merge branch 'redo-fragment-multithreading' into multithreaded-fragme…
burgerindividual Sep 18, 2020
57d7116
make AvailableFragmentCache a Deque, other small changes
burgerindividual Sep 21, 2020
15b1f61
remove accidental inverse
burgerindividual Sep 21, 2020
bb468eb
merge fragment state machine stuff
burgerindividual Sep 22, 2020
62b8f9a
Update SelfExpiringSoftHashMap.java
burgerindividual Sep 22, 2020
4d1c313
update docs
burgerindividual Sep 22, 2020
f3b7e01
Update Fragment.java
burgerindividual Sep 22, 2020
0f1c984
Merge branch 'redo-fragment-multithreading' into multithreaded-fragme…
burgerindividual Sep 22, 2020
97b466b
more cache changes
burgerindividual Sep 24, 2020
b7fa6f6
change some method structure
burgerindividual Sep 24, 2020
6206b12
allowed the availabe cache to have a similar consumer clean method
burgerindividual Sep 24, 2020
1f6307a
Merge branch 'redo-fragment-multithreading' into multithreaded-fragme…
burgerindividual Sep 26, 2020
f73ef52
add settings to change cache times
burgerindividual Sep 27, 2020
a1748e1
Merge branch 'master' into multithreaded-fragment-cache
burgerindividual Oct 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/amidst/AmidstSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class AmidstSettings {
public final Setting<Boolean> showDebug;
public final Setting<Boolean> useHybridScaling;
public final Setting<Integer> threads;
public final Setting<Integer> offscreenCacheTime;
public final Setting<Integer> availableCacheTime;
public final Setting<AmidstLookAndFeel> lookAndFeel;

public final Setting<String> lastProfile;
Expand Down Expand Up @@ -76,6 +78,8 @@ public AmidstSettings(Preferences preferences) {
showDebug = Setting.createBoolean( preferences, "showDebug", false);
useHybridScaling = Setting.createBoolean( preferences, "useHybridScaling", true);
threads = Setting.createInteger( preferences, "threads", (Runtime.getRuntime().availableProcessors() / 2) + 1);
offscreenCacheTime = Setting.createInteger( preferences, "offscreenCacheTime", 30000);
availableCacheTime = Setting.createInteger( preferences, "availableCacheTime", 60000);
lookAndFeel = Setting.createEnum( preferences, "lookAndFeel", AmidstLookAndFeel.DEFAULT);

lastProfile = Setting.createString( preferences, "profile", "");
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/amidst/PerApplicationInjector.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ public PerApplicationInjector(CommandLineParameters parameters, AmidstMetaData m
.createLocalAndStartDownloadingRemote(threadMaster.getWorkerExecutor());
this.layerBuilder = new LayerBuilder();
this.zoom = new Zoom(settings.maxZoom);
this.fragmentManager = new FragmentManager(layerBuilder.getConstructors(), layerBuilder.getNumberOfLayers(), settings.threads);
this.fragmentManager = new FragmentManager(
layerBuilder.getConstructors(),
layerBuilder.getNumberOfLayers(),
settings.threads,
settings.availableCacheTime,
settings.offscreenCacheTime);
this.biomeSelection = new BiomeSelection();
this.application = new Application(
preferredLauncherProfile,
Expand Down
96 changes: 96 additions & 0 deletions src/main/java/amidst/fragment/AvailableFragmentCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package amidst.fragment;

import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.function.Consumer;

import amidst.documentation.AmidstThread;
import amidst.documentation.CalledOnlyBy;
import amidst.documentation.NotThreadSafe;
import amidst.fragment.constructor.FragmentConstructor;
import amidst.settings.Setting;
import amidst.util.SoftExpiringReference;

@NotThreadSafe
public class AvailableFragmentCache {
private final ConcurrentLinkedDeque<SoftExpiringReference<Fragment>> cache = new ConcurrentLinkedDeque<>();

private final Iterable<FragmentConstructor> constructors;
private final int numberOfLayers;
private final Setting<Integer> availableCacheTime;

@CalledOnlyBy(AmidstThread.EDT)
public AvailableFragmentCache(
Iterable<FragmentConstructor> constructors,
int numberOfLayers,
Setting<Integer> availableCacheTime) {
this.constructors = constructors;
this.numberOfLayers = numberOfLayers;
this.availableCacheTime = availableCacheTime;
}

@CalledOnlyBy(AmidstThread.EDT)
public Fragment getOrCreate() {
Fragment fragment = null;
while(!isEmpty() && (fragment = poll()) == null); // try to retrieve a non-null fragment from the cache

if(fragment == null) {
fragment = new Fragment(numberOfLayers);
construct(fragment);
}

return fragment;
}

@CalledOnlyBy(AmidstThread.EDT)
private Fragment poll() {
SoftExpiringReference<Fragment> value = (SoftExpiringReference<Fragment>) cache.pollFirst();
return value != null ? value.getValue() : null;
}

/**
* Fragments that are used when calling this method should already
* be recycled.
*/
@CalledOnlyBy(AmidstThread.EDT)
public void put(Fragment fragment) {
cache.addLast(new SoftExpiringReference<>(fragment, availableCacheTime.get()));
}

@CalledOnlyBy(AmidstThread.EDT)
private void construct(Fragment fragment) {
for (FragmentConstructor constructor : constructors) {
constructor.construct(fragment);
}
}

public synchronized void clean() {
clean(null);
}

public synchronized void clean(Consumer<Fragment> expiredConsumer) {
Iterator<SoftExpiringReference<Fragment>> fragRefIterator = cache.iterator();
for (SoftExpiringReference<Fragment> fragRef : (Iterable<SoftExpiringReference<Fragment>>) () -> fragRefIterator) {
Fragment realFrag = null;
boolean isNull = (fragRef == null || (realFrag = fragRef.getValue()) == null);

if (isNull || fragRef.getDelayMillis() < 0) {
fragRefIterator.remove();
// pass to consumer if expired and not null
if(!isNull && expiredConsumer != null) {
expiredConsumer.accept(realFrag);
}
}
}
}

@CalledOnlyBy(AmidstThread.EDT)
public int size() {
return cache.size();
}

@CalledOnlyBy(AmidstThread.EDT)
public boolean isEmpty() {
return cache.size() <= 0;
}
}
82 changes: 30 additions & 52 deletions src/main/java/amidst/fragment/Fragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,19 @@
* setters. </br>
* </br>
* The life-cycle of a Fragment is quite complex to prevent the garbage
* collection from running too often. When a fragment is no longer needed it
* will be kept available in a queue, so it can be reused later on. The
* life-cycle consists of the three flags: isInitialized, isLoading, and
* isLoaded. isInitialized can be set to true from any thread, however setting
* isInitialized to false as well as any modification to isLoading and isLoaded
* will always be called from the fragment loading thread, to ensure a
* consistent state. Also, isInitialized will only be set to true again after it
* was set to false. It is not possible that isLoading or isLoaded is true while
* isInitialized is false. It is also not possible for isLoaded to be true while
* isLoading is true. </br>
* </br>
* It is possible that a thread that uses the data in the fragment continues to
* use them after isLoaded is set to false. However, all write operations are
* called from either the fragment loading thread, the threads from
* {@link FragmentManager#fragWorkers}, or the EDT during theconstruction of
* the fragment. While the fragment is constructed it will only be accessible
* by one thread. An exception to that rule is the instance variable alpha.
* It is altered from the drawing thread, however this should not cause any
* issues. </br>
* </br>
* Immediately after a new instance of this class is created, it is passed to
* all FragmentConstructors. At that point in time, no other thread can access
* the fragment, so the whole construction process is single-threaded. After the
* fragment is constructed it will be available to use in the fragment graph. As
* soon as it is requested, its isInitialized variable will be set to true by
* the requesting thread. Also, it is enqueued to the loading queue. Note, that
* the fragment is still not loaded, but used in the fragment graph and thus
* collection from running too often. The cycle consists of four states:
* uninitialized, initialized, loading, and loaded. These all govern what can
* be done to the fragment and any given time. A fragment starts off as
* uninitialized on creation. Immediately after a new fragment is created, it
* is passed to all FragmentConstructors by the EDT. At that point in time, no
* other thread can access the fragment, so the whole construction process is
* single-threaded. After the fragment is constructed, its state gets set to
* initialized, and is put in both the fragment graph in the loading queue. Note,
* that the fragment is still not loaded, but used in the fragment graph and thus
* used by the {@link Drawer}. Sometime after the fragment was requested, it
* will go through the loading process, because it was enqueued to the loading
* queue. </br>
* will go through the loading process because it was enqueued to the loading
* queue. The fragment loader and worker threads are the only threads
* that can modify its state beyond this point.</br>
* </br>
* During the loading process, the fragment loader thread will first check to
* make sure that there are threads open in the fragment worker thread pool. If
Expand All @@ -61,31 +44,26 @@
* is set to true. This is to make sure that only one thread is loading the
* fragment at a time. </br>
* </br>
* When this is done, the isLoaded variable will be set to true. This allows the
* When this is done, the state will be set to loaded. This allows the
* drawer to actually draw the fragment. The complete drawing process is
* executed in the event dispatch thread. When the fragment is no longer visible
* on the screen it will be removed from the fragment graph. However, since it
* holds a data-structure that is quite heavy to allocate and garbage-collect,
* the fragment will be recycled so it can be reused later. This recycling is
* done by enqueuing the fragment to the recycle queue. The recycle queue is
* processed by the fragment loading thread with a very high priority. Even
* though the fragment loading thread only calls the method
* {@link Fragment#recycle()} and enqueues the fragment to the available queue,
* it is important that this is done by the fragment loading queue. This is,
* because if any other thread sets the isLoaded variable to false, it might be
* set to true by the fragment loading thread afterwards, because the fragment
* was not yet loaded. This problem is solved by modifying the isLoaded variable
* only in the fragment loading thread. This issue only arises, since the
* fragment is already used in the fragment graph, before it is loaded. As soon
* as it is used in the fragment graph it, can be recycled. This often leads to
* a situation where a not yet loaded fragment gets recycled. The isInitialized
* variable is altered by the thread that requests the new fragment, which is
* different from the fragment loading thread. This is not an issue, since all
* fragments in the available queue have both variables isInitialized and
* isLoaded set to false. They are also not used in the fragment graph or
* enqueued in the recycle queue. Therefore, there cannot be a race condition
* because the isInitialized variable will only be set to false when it is
* recycled.
* on the screen, a check is done to determine how to use it. If the fragment is
* loading or loaded, it gets sent to the off-screen cache, where it will be able
* to exist for a set amount of time without being refreshed before it gets
* recycled. If the fragment isn't loaded or loading, it also gets recycled.
* When a fragment is recycled, its state gets reset to initialized and it gets
* sent to the available cache to be re-used. If a fragment stays in the available
* queue for too long, it gets cleared and eventually garbage collected.</br>
* </br>
* It is possible that a thread that uses the data in the fragment continues to
* use them after its state gets reset to uninitialized. However, all write
* operations are called from either the fragment loading thread, the threads from
* {@link FragmentManager#fragWorkers}, or the EDT during the construction of
* the fragment. While the fragment is constructed it will only be accessible
* by one thread. An exception to that rule is the instance variable alpha.
* It is altered from the drawing thread, however this should not cause any
* issues. </br>
* </br>
*/
@ThreadSafe
public class Fragment {
Expand Down
80 changes: 0 additions & 80 deletions src/main/java/amidst/fragment/FragmentCache.java

This file was deleted.

1 change: 1 addition & 0 deletions src/main/java/amidst/fragment/FragmentGraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public void dispose() {

@CalledOnlyBy(AmidstThread.EDT)
private void recycleAll() {
fragmentManager.invalidateCaches();
topLeftFragment.ifInitialized(f -> f.recycleAll(fragmentManager));
}

Expand Down
12 changes: 6 additions & 6 deletions src/main/java/amidst/fragment/FragmentGraphItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ private void deleteFirstRow(FragmentManager manager) {
while (current != null) {
current.disconnectBelow();
FragmentGraphItem right = current.disconnectRight();
current.recycle(manager);
current.retire(manager);
current = right;
}
}
Expand All @@ -225,7 +225,7 @@ private void deleteLastRow(FragmentManager manager) {
while (current != null) {
current.disconnectAbove();
FragmentGraphItem right = current.disconnectRight();
current.recycle(manager);
current.retire(manager);
current = right;
}
}
Expand All @@ -236,7 +236,7 @@ private void deleteFirstColumn(FragmentManager manager) {
while (current != null) {
current.disconnectRight();
FragmentGraphItem below = current.disconnectBelow();
current.recycle(manager);
current.retire(manager);
current = below;
}
}
Expand All @@ -247,7 +247,7 @@ private void deleteLastColumn(FragmentManager manager) {
while (current != null) {
current.disconnectLeft();
FragmentGraphItem below = current.disconnectBelow();
current.recycle(manager);
current.retire(manager);
current = below;
}
}
Expand Down Expand Up @@ -382,7 +382,7 @@ private FragmentGraphItem createFragmentGraphItem(FragmentManager manager, int x
}

@CalledOnlyBy(AmidstThread.EDT)
private void recycle(FragmentManager manager) {
manager.recycleFragment(fragment);
private void retire(FragmentManager manager) {
manager.retireFragment(fragment);
}
}
Loading