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

Fix SideEffectSet inconsistencies, improve perf #2620

Merged
merged 2 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ target
forge-download
out
run
.jqwik-database

/dependency-reduced-pom.xml
*-private.sh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ configure<CheckstyleExtension> {
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
useJUnitPlatform {
includeEngines("junit-jupiter", "jqwik")
}
}

dependencies {
"compileOnly"(stringyLibs.getLibrary("jsr305"))
"testImplementation"(platform(stringyLibs.getLibrary("junit-bom")))
"testImplementation"(stringyLibs.getLibrary("junit-jupiter-api"))
"testImplementation"(stringyLibs.getLibrary("junit-jupiter-params"))
"testImplementation"(stringyLibs.getLibrary("jqwik"))
"testImplementation"(platform(stringyLibs.getLibrary("mockito-bom")))
"testImplementation"(stringyLibs.getLibrary("mockito-core"))
"testImplementation"(stringyLibs.getLibrary("mockito-junit-jupiter"))
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ junit-jupiter-api.module = "org.junit.jupiter:junit-jupiter-api"
junit-jupiter-params.module = "org.junit.jupiter:junit-jupiter-params"
junit-jupiter-engine.module = "org.junit.jupiter:junit-jupiter-engine"

jqwik = "net.jqwik:jqwik:1.9.0"

mockito-bom = "org.mockito:mockito-bom:5.11.0"
mockito-core.module = "org.mockito:mockito-core"
mockito-junit-jupiter.module = "org.mockito:mockito-junit-jupiter"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ public void setSideEffectApplier(SideEffectSet sideEffectSet) {
*/
@Deprecated
public boolean hasFastMode() {
return sideEffectExtent != null && this.sideEffectExtent.getSideEffectSet().doesApplyAny();
return sideEffectExtent != null && !this.sideEffectExtent.getSideEffectSet().doesApplyAny();
}

public SideEffectSet getSideEffectApplier() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@

package com.sk89q.worldedit.util;

import com.google.common.base.Verify;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
Expand All @@ -37,33 +38,73 @@ public class SideEffectSet {
.collect(Collectors.toMap(Function.identity(), state -> SideEffect.State.OFF))
);

private final Map<SideEffect, SideEffect.State> sideEffects;
private final Set<SideEffect> appliedSideEffects;
private final boolean appliesAny;
static {
Verify.verify(
SideEffect.State.values().length == 3,
"Implementation requires specifically 3 values in the SideEffect.State enum"
);
int maxEffects = Integer.SIZE / 2;
Verify.verify(
SideEffect.values().length <= maxEffects,
"Implementation requires less than " + maxEffects + " side effects"
);
Verify.verify(
SideEffect.State.OFF.ordinal() == 0,
"Implementation requires SideEffect.State.OFF to be the first value"
);
}

private static int shift(SideEffect effect) {
return effect.ordinal() * 2;
}

private static int computeSideEffectsBitmap(Map<SideEffect, SideEffect.State> sideEffects) {
int sideEffectsBitmap = 0;
for (SideEffect effect : SideEffect.values()) {
SideEffect.State state = sideEffects.getOrDefault(effect, effect.getDefaultValue());
sideEffectsBitmap |= (state.ordinal() << shift(effect));
}
return sideEffectsBitmap;
}

/**
* Side-effects and state are encoded into this field, 2 bits per side-effect. Least-significant bit is first.
*/
private final int sideEffectsBitmap;
private final Set<SideEffect> sideEffectsToApply;

public SideEffectSet(Map<SideEffect, SideEffect.State> sideEffects) {
this.sideEffects = Maps.immutableEnumMap(sideEffects);

appliedSideEffects = sideEffects.entrySet()
.stream()
.filter(entry -> entry.getValue() != SideEffect.State.OFF)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
appliesAny = !appliedSideEffects.isEmpty();
this(computeSideEffectsBitmap(sideEffects));
}

private SideEffectSet(int sideEffectsBitmap) {
this.sideEffectsBitmap = sideEffectsBitmap;
var sideEffectsToApply = EnumSet.noneOf(SideEffect.class);
for (SideEffect effect : SideEffect.values()) {
if (shouldApply(effect)) {
sideEffectsToApply.add(effect);
}
}
this.sideEffectsToApply = Sets.immutableEnumSet(sideEffectsToApply);
}

public SideEffectSet with(SideEffect sideEffect, SideEffect.State state) {
Map<SideEffect, SideEffect.State> entries = this.sideEffects.isEmpty() ? Maps.newEnumMap(SideEffect.class) : new EnumMap<>(this.sideEffects);
entries.put(sideEffect, state);
return new SideEffectSet(entries);
int mask = 0b11 << shift(sideEffect);
int newState = (state.ordinal() << shift(sideEffect)) & mask;
int newBitmap = (sideEffectsBitmap & ~mask) | newState;
return new SideEffectSet(newBitmap);
}

public boolean doesApplyAny() {
return this.appliesAny;
return sideEffectsBitmap != 0;
}

public SideEffect.State getState(SideEffect effect) {
return sideEffects.getOrDefault(effect, effect.getDefaultValue());
return SideEffect.State.values()[getRawState(effect)];
}

private int getRawState(SideEffect effect) {
return (sideEffectsBitmap >> shift(effect)) & 0b11;
}

/**
Expand All @@ -77,11 +118,11 @@ public SideEffect.State getState(SideEffect effect) {
* @return Whether it should apply
*/
public boolean shouldApply(SideEffect effect) {
return getState(effect) != SideEffect.State.OFF;
return getRawState(effect) != 0;
}

public Set<SideEffect> getSideEffectsToApply() {
return this.appliedSideEffects;
return sideEffectsToApply;
}

public static SideEffectSet defaults() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.sk89q.worldedit.util;

import net.jqwik.api.ForAll;
import net.jqwik.api.Property;

import java.util.Map;

public class SideEffectSetTest {
@Property
public boolean stateOrDefaultIsCorrect(
@ForAll Map<SideEffect, SideEffect.State> stateMap,
@ForAll SideEffect effectToTest
) {
SideEffectSet set = new SideEffectSet(stateMap);
return set.getState(effectToTest) == stateMap.getOrDefault(effectToTest, effectToTest.getDefaultValue());
}

@Property
public boolean shouldApplyUnlessOff(
@ForAll Map<SideEffect, SideEffect.State> stateMap,
@ForAll SideEffect effectToTest
) {
SideEffectSet set = new SideEffectSet(stateMap);
return set.shouldApply(effectToTest)
== (stateMap.getOrDefault(effectToTest, effectToTest.getDefaultValue()) != SideEffect.State.OFF);
}

@Property
public boolean withChangesState(
@ForAll Map<SideEffect, SideEffect.State> stateMap,
@ForAll SideEffect effectToTest,
@ForAll SideEffect.State stateToSet
) {
SideEffectSet set = new SideEffectSet(stateMap).with(effectToTest, stateToSet);
return set.getState(effectToTest) == stateToSet;
}

@Property
public boolean anyShouldApplyEqualsDoesApplyAny(@ForAll Map<SideEffect, SideEffect.State> stateMap) {
SideEffectSet set = new SideEffectSet(stateMap);
boolean anyShouldApply = false;
for (SideEffect effect : SideEffect.values()) {
if (set.shouldApply(effect)) {
anyShouldApply = true;
break;
}
}
return anyShouldApply == set.doesApplyAny();
}

@Property
public boolean shouldApplyEqualsApplySetContains(
@ForAll Map<SideEffect, SideEffect.State> stateMap,
@ForAll SideEffect effectToTest
) {
SideEffectSet set = new SideEffectSet(stateMap);
return set.shouldApply(effectToTest) == set.getSideEffectsToApply().contains(effectToTest);
}
}
2 changes: 2 additions & 0 deletions worldedit-core/src/test/resources/junit-platform.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=same_thread
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.config.dynamic.factor=4

jqwik.tries.default = 10000