Skip to content

Commit

Permalink
refactor: refactor claim caching, improve perf
Browse files Browse the repository at this point in the history
O(n) to O(1). Thanks @Al3xDev for the idea.
  • Loading branch information
WiIIiam278 committed Apr 4, 2024
1 parent aff0748 commit 9e3927e
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;

/**
Expand Down Expand Up @@ -415,11 +414,7 @@ public void updateClaim(@NotNull TownClaim claim, @NotNull World world) throws I
if (claim.isAdminClaim(plugin)) {
return;
}

final ConcurrentLinkedQueue<Claim> claims = claimWorld.getClaims()
.computeIfAbsent(claim.town().getId(), k -> new ConcurrentLinkedQueue<>());
claims.removeIf(c -> c.getChunk().equals(claim.claim().getChunk()));
claims.add(claim.claim());
claimWorld.replaceClaim(claim, plugin);
plugin.getDatabase().updateClaimWorld(claimWorld);
});
}
Expand Down
27 changes: 14 additions & 13 deletions common/src/main/java/net/william278/husktowns/claim/Chunk.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
package net.william278.husktowns.claim;

import com.google.gson.annotations.Expose;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.cloplib.operation.OperationChunk;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Getter
@NoArgsConstructor
public class Chunk implements OperationChunk {

@Expose
Expand All @@ -41,18 +45,6 @@ public static Chunk at(int x, int z) {
return new Chunk(x, z);
}

@SuppressWarnings("unused")
private Chunk() {
}

public int getX() {
return x;
}

public int getZ() {
return z;
}

@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) return true;
Expand All @@ -66,12 +58,21 @@ public String toString() {
return "(x: " + x + ", z: " + z + ")";
}

/**
* Get the long position of the chunk
*
* @return the long position
*/
public long asLong() {
return ((long) x << 32) | (z & 0xffffffffL);
}

public int distanceBetween(@NotNull Chunk chunk) {
return Math.abs(x - chunk.x) + Math.abs(z - chunk.z);
}

public boolean contains(Position position) {
return position.getX() >= x * 16 && position.getX() < (x + 1) * 16
&& position.getZ() >= z * 16 && position.getZ() < (z + 1) * 16;
&& position.getZ() >= z * 16 && position.getZ() < (z + 1) * 16;
}
}
142 changes: 90 additions & 52 deletions common/src/main/java/net/william278/husktowns/claim/ClaimWorld.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,67 +19,60 @@

package net.william278.husktowns.claim;

import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.husktowns.HuskTowns;
import net.william278.husktowns.town.Town;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

@NoArgsConstructor
public class ClaimWorld {

@Getter
private int id;
@Expose
private ConcurrentHashMap<Integer, ConcurrentLinkedQueue<Claim>> claims;

@SerializedName("claims")
private ConcurrentMap<Integer, ConcurrentLinkedQueue<Claim>> claims = Maps.newConcurrentMap();
@Expose
@SerializedName("admin_claims")
private ConcurrentLinkedQueue<Claim> adminClaims;
private ConcurrentLinkedQueue<Claim> adminClaims = Queues.newConcurrentLinkedQueue();

@Expose(deserialize = false, serialize = false)
private transient Map<Long, CachedClaim> cachedClaims = Maps.newConcurrentMap();

private ClaimWorld(int id, @NotNull Map<Integer, List<Claim>> claims, @NotNull List<Claim> adminClaims) {
this.id = id;
this.adminClaims = new ConcurrentLinkedQueue<>(adminClaims);
this.claims = new ConcurrentHashMap<>();
claims.forEach((key, value) -> this.claims.put(key, new ConcurrentLinkedQueue<>(value)));
this.cacheClaims(claims, adminClaims);
}

@NotNull
public static ClaimWorld of(int id, @NotNull Map<Integer, List<Claim>> claims, @NotNull List<Claim> adminClaims) {
return new ClaimWorld(id, claims, adminClaims);
}

@SuppressWarnings("unused")
private ClaimWorld() {
private void cacheClaims(@NotNull Map<Integer, List<Claim>> claims, @NotNull List<Claim> adminClaims) {
claims.forEach((key, value) -> {
this.claims.put(key, new ConcurrentLinkedQueue<>(value));
value.forEach(claim -> this.cachedClaims.put(claim.getChunk().asLong(), new CachedClaim(key, claim)));
});
adminClaims.forEach(claim -> {
this.cachedClaims.put(claim.getChunk().asLong(), new CachedClaim(-1, claim));
this.adminClaims.add(claim);
});
}

public Optional<TownClaim> getClaimAt(@NotNull Chunk chunk, @NotNull HuskTowns plugin) {
return claims.entrySet().stream()
.filter(entry -> entry.getValue().stream().anyMatch(claim -> claim.getChunk().equals(chunk)))
.findFirst()
.flatMap(entry -> entry.getValue().stream()
.filter(claim -> claim.getChunk().equals(chunk))
.findFirst()
.flatMap(claim -> plugin.findTown(entry.getKey())
.map(town1 -> new TownClaim(town1, claim))))
.or(() -> adminClaims.stream()
.filter(claim -> claim.getChunk().equals(chunk))
.findFirst()
.map(claim -> new TownClaim(plugin.getAdminTown(), claim)));
}

/**
* Get the ID of the claim world
*
* @return the ID of the claim world
*/
public int getId() {
return id;
return Optional.ofNullable(cachedClaims.get(chunk.asLong())).map(cached -> cached.getTownClaim(plugin));
}

/**
Expand All @@ -97,7 +90,7 @@ public void updateId(int id) {
* @return the number of claims in this world
*/
public int getClaimCount() {
return claims.values().stream().mapToInt(ConcurrentLinkedQueue::size).sum() + getAdminClaimCount();
return cachedClaims.size();
}

/**
Expand All @@ -109,6 +102,29 @@ public int getAdminClaimCount() {
return adminClaims.size();
}

@NotNull
public List<TownClaim> getTownClaims(int townId, @NotNull HuskTowns plugin) {
return cachedClaims.values().stream()
.filter(cachedClaim -> cachedClaim.townId == townId)
.map(cachedClaim -> cachedClaim.getTownClaim(plugin))
.collect(Collectors.toList());
}

@NotNull
@Unmodifiable
public Map<Integer, List<Claim>> getClaims() {
return claims.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> new ArrayList<>(entry.getValue())));
}

@NotNull
public List<TownClaim> getClaims(@NotNull HuskTowns plugin) {
return cachedClaims.values().stream()
.map(cachedClaim -> cachedClaim.getTownClaim(plugin))
.collect(Collectors.toList());
}


/**
* Remove claims by a town on this world
*
Expand All @@ -119,6 +135,7 @@ public int removeTownClaims(int townId) {
if (claims.containsKey(townId)) {
int claimCount = claims.get(townId).size();
claims.remove(townId);
cachedClaims.values().removeIf(cachedClaim -> cachedClaim.townId == townId);
return claimCount;
}
return 0;
Expand All @@ -129,18 +146,39 @@ public void addClaim(@NotNull TownClaim townClaim) {
claims.put(townClaim.town().getId(), new ConcurrentLinkedQueue<>());
}
claims.get(townClaim.town().getId()).add(townClaim.claim());
cachedClaims.put(townClaim.claim().getChunk().asLong(), new CachedClaim(townClaim.town().getId(), townClaim.claim()));
}

public void replaceClaim(@NotNull TownClaim townClaim, @NotNull HuskTowns plugin) {
final Claim claim = townClaim.claim();
if (townClaim.isAdminClaim(plugin)) {
adminClaims.removeIf(c -> c.getChunk().equals(claim.getChunk()));
adminClaims.add(claim);
cachedClaims.put(claim.getChunk().asLong(), new CachedClaim(-1, claim));
} else if (claims.containsKey(townClaim.town().getId())) {
claims.get(townClaim.town().getId()).removeIf(c -> c.getChunk().equals(claim.getChunk()));
claims.get(townClaim.town().getId()).add(claim);
cachedClaims.put(claim.getChunk().asLong(), new CachedClaim(townClaim.town().getId(), claim));
}
}

public void addAdminClaim(@NotNull Claim claim) {
cachedClaims.put(claim.getChunk().asLong(), new CachedClaim(-1, claim));
adminClaims.add(claim);
}

public void removeClaim(@NotNull Town town, @NotNull Chunk chunk) {
cachedClaims.remove(chunk.asLong());
if (claims.containsKey(town.getId())) {
claims.get(town.getId()).removeIf(claim -> claim.getChunk().equals(chunk));
}
}

public void removeAdminClaim(@NotNull Chunk chunk) {
cachedClaims.remove(chunk.asLong());
adminClaims.removeIf(claim -> claim.getChunk().equals(chunk));
}

@NotNull
public List<TownClaim> getClaimsNear(@NotNull Chunk chunk, int radius, @NotNull HuskTowns plugin) {
if (radius <= 0) {
Expand All @@ -161,24 +199,12 @@ public List<TownClaim> getAdjacentClaims(@NotNull Chunk chunk, @NotNull HuskTown
return getClaimsNear(chunk, 1, plugin);
}

public void removeAdminClaim(@NotNull Chunk chunk) {
adminClaims.removeIf(claim -> claim.getChunk().equals(chunk));
}

@NotNull
public ConcurrentHashMap<Integer, ConcurrentLinkedQueue<Claim>> getClaims() {
return claims;
}

@NotNull
public List<TownClaim> getClaims(@NotNull HuskTowns plugin) {
List<TownClaim> townClaims = new ArrayList<>();
claims.forEach((townId, claimList) -> {
Optional<Town> town = plugin.findTown(townId);
town.ifPresent(value -> claimList.forEach(claim -> townClaims.add(new TownClaim(value, claim))));
});
adminClaims.forEach(claim -> townClaims.add(new TownClaim(plugin.getAdminTown(), claim)));
return townClaims;
public boolean pruneOrphanClaims(@NotNull HuskTowns plugin) {
return new HashMap<>(claims).keySet().stream()
.filter(town -> plugin.findTown(town).isEmpty())
.map(this::removeTownClaims)
.anyMatch(count -> count > 0);
}

@Override
Expand All @@ -189,4 +215,16 @@ public boolean equals(Object obj) {
return id == claimWorld.id;
}

private record CachedClaim(int townId, @NotNull Claim claim) {
@NotNull
TownClaim getTownClaim(@NotNull HuskTowns plugin) {
if (townId == -1) {
return new TownClaim(plugin.getAdminTown(), claim);
}
return plugin.findTown(townId)
.map(town -> new TownClaim(town, claim))
.orElseThrow(() -> new IllegalStateException("Claim has invalid town ID: " + townId));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -191,7 +190,7 @@ public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
final Component mapGrid = plugin.getWorlds().stream()
.map(world -> Map.entry(world, plugin.getClaimWorld(world)
.map(claimWorld -> claimWorld.getClaims().get(town.getId()))
.orElse(new ConcurrentLinkedQueue<>()).stream()
.orElse(new ArrayList<>()).stream()
.map(claim -> new TownClaim(town, claim))
.toList()))
.flatMap((worldMap) -> worldMap.getValue().stream()
Expand Down
19 changes: 6 additions & 13 deletions common/src/main/java/net/william278/husktowns/hook/MapHook.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.jetbrains.annotations.NotNull;

import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;

public abstract class MapHook extends Hook {

Expand All @@ -39,23 +38,17 @@ protected MapHook(@NotNull HuskTowns plugin, @NotNull String name) {
public abstract void removeClaimMarker(@NotNull TownClaim claim, @NotNull World world);

public final void removeClaimMarkers(@NotNull Town town) {
plugin.getWorlds().forEach(world -> plugin.getClaimWorld(world).ifPresent(claimWorld -> {
final List<TownClaim> claims = claimWorld.getClaims()
.getOrDefault(town.getId(), new ConcurrentLinkedQueue<>()).stream()
.map(claim -> new TownClaim(town, claim)).toList();
removeClaimMarkers(claims, world);
}));
plugin.getWorlds().forEach(world -> plugin.getClaimWorld(world).ifPresent(
claimWorld -> removeClaimMarkers(claimWorld.getTownClaims(town.getId(), plugin), world)
));
}

public abstract void setClaimMarkers(@NotNull List<TownClaim> claims, @NotNull World world);

public final void setClaimMarkers(@NotNull Town town) {
plugin.getWorlds().forEach(world -> plugin.getClaimWorld(world).ifPresent(claimWorld -> {
final List<TownClaim> claims = claimWorld.getClaims()
.getOrDefault(town.getId(), new ConcurrentLinkedQueue<>()).stream()
.map(claim -> new TownClaim(town, claim)).toList();
setClaimMarkers(claims, world);
}));
plugin.getWorlds().forEach(world -> plugin.getClaimWorld(world).ifPresent(
claimWorld -> setClaimMarkers(claimWorld.getTownClaims(town.getId(), plugin), world)
));
}

public abstract void removeClaimMarkers(@NotNull List<TownClaim> claims, @NotNull World world);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import java.sql.*;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;

/**
Expand Down Expand Up @@ -250,12 +249,7 @@ protected Map<ServerWorld, ClaimWorld> getConvertedClaimWorlds() {
continue;
}

final int townId = town.get().getId();
if (claimWorld.getClaims().containsKey(townId)) {
claimWorld.getClaims().get(townId).add(claim);
} else {
claimWorld.getClaims().put(townId, new ConcurrentLinkedQueue<>(List.of(claim)));
}
claimWorld.addClaim(new TownClaim(town.get(), claim));
claimWorlds.replaceAll((k, v) -> k.equals(serverWorld) ? claimWorld : v);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,9 @@ default void pruneOrphanClaims() {
getPlugin().log(Level.INFO, "Validating and pruning orphan claims...");
final LocalTime startTime = LocalTime.now();

getPlugin().getClaimWorlds().values().forEach(world -> {
world.getClaims().keySet().removeIf(
claim -> getPlugin().getTowns().stream().noneMatch(town -> town.getId() == claim)
);
getPlugin().getDatabase().updateClaimWorld(world);
});
getPlugin().getClaimWorlds().values().stream()
.filter(world -> world.pruneOrphanClaims(getPlugin()))
.forEach(w -> getPlugin().getDatabase().updateClaimWorld(w));

getPlugin().log(Level.INFO, "Successfully validated and pruned orphan claims in " +
(ChronoUnit.MILLIS.between(startTime, LocalTime.now()) / 1000d) + " seconds");
Expand Down

0 comments on commit 9e3927e

Please sign in to comment.