Skip to content
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
26 changes: 15 additions & 11 deletions docs/performance-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,16 +362,16 @@ The dependency surface is deliberately fixed:
| External client runtime | Node `22.23.1` |
| Protocol implementation | `minecraft-protocol` `1.66.2`, locked by npm integrity |
| Paper | `1.20.1` build `196`, SHA-256 `234a9b32098100c6fc116664d64e36ccdb58b5b649af0f80bcccb08b0255eaea` |
| CraftEngine for plugin runs | Paper `26.7.3` (`Len451or`), SHA-512 from the committed artifact lock |
| CraftEngine for plugin runs | Paper `26.7.4` (`aINSQrXC`), SHA-512 from the committed artifact lock |

`perf/live/artifacts.lock.json` contains immutable URLs, byte sizes and hashes. Downloads are
written to a temporary file, size/hash verified, fsynced, then atomically moved into place.
The Paper URL is the official Paper downloads-service object. The optional CraftEngine entry
is artifact `craftengine-paper-26.7.3`, the latest Paper/Folia/Purpur file published for that
is artifact `craftengine-paper-26.7.4`, the latest Paper/Folia/Purpur file published for that
version. Do not substitute the same-version Bukkit/Spigot file: Modrinth marks that separate
artifact for Minecraft 26.x only, while the locked Paper file explicitly includes Paper 1.20.1
through 26.2. Its committed metadata source is
`https://api.modrinth.com/v2/version/Len451or`.
`https://api.modrinth.com/v2/version/aINSQrXC`.

### What is actually counted

Expand Down Expand Up @@ -424,13 +424,16 @@ or substitutes a stub named `CraftEngine`.

```powershell
python perf/live/download_locked.py `
--artifact craftengine-paper-26.7.3 `
--destination build/live-cache/craft-engine-paper-plugin-26.7.3.jar
--artifact craftengine-paper-26.7.4 `
--destination build/live-cache/craft-engine-paper-plugin-26.7.4.jar
python perf/live/run_server.py `
--paper-jar build/live-cache/server.jar `
--plugin-jar build/libs/mahjong-paper-1.5.0.jar `
--craftengine-jar build/live-cache/craft-engine-paper-plugin-26.7.3.jar `
--plugin-config perf/live/fixtures/mahjongpaper-smoke-config.yml `
--craftengine-jar build/live-cache/craft-engine-paper-plugin-26.7.4.jar `
--required-plugin MahjongPaper `
--setup-command 'execute at MahjongPerfBot run fill ~-16 ~-1 ~-16 ~16 ~-1 ~16 minecraft:stone' `
--setup-command 'execute at MahjongPerfBot run fill ~-16 ~ ~-16 ~16 ~10 ~16 minecraft:air' `
--scenario-name majsoul-hanchan-four-bot-spectator `
--scenario-command "/mahjong botmatch MAJSOUL_HANCHAN" `
--scenario-ready-pattern 'Round .+ \| Turn .+ \| Wall [1-9][0-9]* \| Spectators 1' `
Expand All @@ -449,11 +452,11 @@ and must observe the committed ready pattern before warmup and measurement begin
traffic is a hard failure, not a fallback to an empty-server keepalive run.

The standalone measurement-infrastructure PR deliberately does not execute this plugin profile
as its required pull-request check. The current foundation baseline does not expose Caffeine to
the plugin classloader on a fresh Paper instance, so the real plugin profile correctly stops at
the required-plugin enablement check. That dependency blocker must be fixed in the foundation
change and then exercised by a stacked run; this harness does not patch the plugin descriptor,
inject a fake dependency, or turn the known failure green.
as its required pull-request check. Plugin runs use the committed fixture config to disable game
room placement restrictions, persistence and ranking equally for base and candidate, then create
a flat, clear table footprint relative to the connected protocol client before starting the
botmatch. The harness still loads the real locked CraftEngine plugin and does not patch plugin
metadata or inject a fake dependency.

### Measurement rules

Expand All @@ -480,6 +483,7 @@ run-manifest.json hashes, versions, ports, timings, shutdown res
paper-summary.json derived TPS/MSPT sample distributions
raw/paper-samples.jsonl exact timestamped Paper command responses plus parsed values
raw/server-stdout.log complete server console output
raw/inputs/plugin-config.yml exact staged plugin fixture configuration, when supplied
raw/instance/logs/latest.log Paper's own latest.log
raw/runtime-jars.json SHA-256 of server/plugin/runtime jars actually loaded
raw/protocol-client/packets.jsonl one record per externally observed Minecraft frame
Expand Down
20 changes: 10 additions & 10 deletions perf/live/artifacts.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
},
"source_metadata": "https://fill.papermc.io/v3/projects/paper/versions/1.20.1/builds"
},
"craftengine-paper-26.7.3": {
"craftengine-paper-26.7.4": {
"kind": "server-plugin",
"project": "craftengine",
"distribution": "paper",
"version": "26.7.3",
"version": "26.7.4",
"release_type": "beta",
"modrinth_version_id": "Len451or",
"modrinth_version_id": "aINSQrXC",
"loaders": [
"folia",
"paper",
Expand Down Expand Up @@ -53,18 +53,18 @@
"26.1.2",
"26.2"
],
"filename": "craft-engine-paper-plugin-26.7.3.jar",
"url": "https://cdn.modrinth.com/data/tRX6FMfQ/versions/Len451or/craft-engine-paper-plugin-26.7.3.jar",
"size": 8907932,
"filename": "craft-engine-paper-plugin-26.7.4.jar",
"url": "https://cdn.modrinth.com/data/tRX6FMfQ/versions/aINSQrXC/craft-engine-paper-plugin-26.7.4.jar",
"size": 8970694,
"hash": {
"algorithm": "sha512",
"digest": "e87351417e4f99504cf0a8868bad03a88b12bc9bda5c36f0ee4c7451e3525f08146b2130b61cd34b222ee41c38ca75aa9ced9ada65bc3540c80aa66c3d39d689"
"digest": "1d6982e325552fd51a9a481252e0c055c32a5b6e3c2d816cf69ca4b2f355a5f7af1abed00ff9d29c08fb32ff293d9d11837ad4372c34675b73682a98b1ec86b0"
},
"published_hashes": {
"sha1": "467fa7317635cf9f76812bd73a47d984c57cf53f",
"sha512": "e87351417e4f99504cf0a8868bad03a88b12bc9bda5c36f0ee4c7451e3525f08146b2130b61cd34b222ee41c38ca75aa9ced9ada65bc3540c80aa66c3d39d689"
"sha1": "179e7b8b191093e41beca56e3d52ae608f46e161",
"sha512": "1d6982e325552fd51a9a481252e0c055c32a5b6e3c2d816cf69ca4b2f355a5f7af1abed00ff9d29c08fb32ff293d9d11837ad4372c34675b73682a98b1ec86b0"
},
"source_metadata": "https://api.modrinth.com/v2/version/Len451or"
"source_metadata": "https://api.modrinth.com/v2/version/aINSQrXC"
}
}
}
19 changes: 19 additions & 0 deletions perf/live/fixtures/mahjongpaper-smoke-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Deterministic live-performance fixture. Base and candidate runs must use this exact file.
database:
enabled: false

tables:
persistence:
enabled: false

gameRooms:
enabled: false
restrictNewTables: false
enterExitMessages: false

ranking:
enabled: false

integrations:
craftengine:
exportBundleOnEnable: true
18 changes: 17 additions & 1 deletion perf/live/run_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
TPS_VALUES = re.compile(r"\*?(\d+(?:\.\d+)?)")
MSPT_TRIPLE = re.compile(r"(\d+(?:\.\d+)?)/(\d+(?:\.\d+)?)/(\d+(?:\.\d+)?)")
READY_LINE = re.compile(r"Done \([^)]+\)! For help, type")
DEFAULT_CRAFTENGINE_ARTIFACT = "craftengine-paper-26.7.3"
DEFAULT_CRAFTENGINE_ARTIFACT = "craftengine-paper-26.7.4"
MAHJONG_TRAFFIC_COMMAND = re.compile(
r"^/mahjong botmatch (MAJSOUL_HANCHAN|MAJSOUL_TONPUU|GB|SICHUAN)$"
)
Expand Down Expand Up @@ -474,6 +474,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--paper-lock", type=Path, default=script_dir / "artifacts.lock.json")
parser.add_argument("--paper-artifact", default="paper-1.20.1-196")
parser.add_argument("--plugin-jar", type=Path)
parser.add_argument("--plugin-config", type=Path)
parser.add_argument("--craftengine-jar", type=Path)
parser.add_argument("--craftengine-artifact", default=DEFAULT_CRAFTENGINE_ARTIFACT)
parser.add_argument("--required-plugin", action="append", default=[])
Expand Down Expand Up @@ -505,6 +506,10 @@ def validate_args(args: argparse.Namespace) -> None:
raise ValueError(f"Paper jar does not exist: {args.paper_jar}")
if args.plugin_jar is not None and not args.plugin_jar.is_file():
raise ValueError(f"Plugin jar does not exist: {args.plugin_jar}")
if args.plugin_config is not None and not args.plugin_config.is_file():
raise ValueError(f"Plugin config does not exist: {args.plugin_config}")
if args.plugin_config is not None and args.plugin_jar is None:
raise ValueError("--plugin-config requires --plugin-jar")
if args.plugin_jar is not None and args.craftengine_jar is None:
raise ValueError("--plugin-jar requires the real locked --craftengine-jar; a stub dependency is not accepted")
if args.craftengine_jar is not None and not args.craftengine_jar.is_file():
Expand Down Expand Up @@ -558,13 +563,19 @@ def main() -> int:
paper_verification = verify_file(args.paper_jar, paper_spec)
input_artifacts: dict[str, Any] = {"paper": paper_verification}
plugin_name = None
plugin_configuration = None
if args.plugin_jar is not None:
plugin_name = jar_plugin_name(args.plugin_jar)
if not plugin_name:
raise ValueError(f"Plugin jar has no readable Paper/Bukkit descriptor: {args.plugin_jar}")
input_artifacts["plugin"] = hash_description(args.plugin_jar)
craftengine_spec = artifact_from_lock(lock, args.craftengine_artifact)
input_artifacts["craftengine"] = verify_file(args.craftengine_jar, craftengine_spec)
if args.plugin_config is not None:
plugin_configuration = hash_description(args.plugin_config)
input_copy = raw_dir / "inputs" / "plugin-config.yml"
input_copy.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(args.plugin_config, input_copy)

ports = reserve_ports(2)
server_port, rcon_port = ports
Expand Down Expand Up @@ -604,6 +615,10 @@ def main() -> int:
plugins_dir.mkdir()
shutil.copy2(args.craftengine_jar, plugins_dir / str(artifact_from_lock(lock, args.craftengine_artifact)["filename"]))
shutil.copy2(args.plugin_jar, plugins_dir / args.plugin_jar.name)
if args.plugin_config is not None:
plugin_data_dir = plugins_dir / plugin_name
plugin_data_dir.mkdir()
shutil.copy2(args.plugin_config, plugin_data_dir / "config.yml")

java_command = [
args.java,
Expand Down Expand Up @@ -785,6 +800,7 @@ def main() -> int:
"artifact_lock": hash_description(args.paper_lock.resolve()),
"world_template": world_description,
"plugin_name": plugin_name,
"plugin_configuration": plugin_configuration,
"setup_commands": args.setup_command,
"scenario": {
"name": args.scenario_name,
Expand Down
22 changes: 11 additions & 11 deletions perf/live/tests/test_download_locked.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,34 @@ def test_committed_paper_lock_has_expected_immutable_object(self) -> None:

def test_committed_craftengine_lock_matches_latest_paper_release(self) -> None:
lock = load_lock(LIVE_DIR / "artifacts.lock.json")
craftengine = artifact_from_lock(lock, "craftengine-paper-26.7.3")
craftengine = artifact_from_lock(lock, "craftengine-paper-26.7.4")
self.assertEqual("craftengine", craftengine["project"])
self.assertEqual("paper", craftengine["distribution"])
self.assertEqual("26.7.3", craftengine["version"])
self.assertEqual("Len451or", craftengine["modrinth_version_id"])
self.assertEqual("26.7.4", craftengine["version"])
self.assertEqual("aINSQrXC", craftengine["modrinth_version_id"])
self.assertIn("paper", craftengine["loaders"])
self.assertIn("1.20.1", craftengine["minecraft_versions"])
self.assertIn("26.1.2", craftengine["minecraft_versions"])
self.assertEqual("craft-engine-paper-plugin-26.7.3.jar", craftengine["filename"])
self.assertEqual("craft-engine-paper-plugin-26.7.4.jar", craftengine["filename"])
self.assertEqual(
"https://cdn.modrinth.com/data/tRX6FMfQ/versions/Len451or/"
"craft-engine-paper-plugin-26.7.3.jar",
"https://cdn.modrinth.com/data/tRX6FMfQ/versions/aINSQrXC/"
"craft-engine-paper-plugin-26.7.4.jar",
craftengine["url"],
)
self.assertEqual(8_907_932, craftengine["size"])
self.assertEqual(8_970_694, craftengine["size"])
self.assertEqual("sha512", craftengine["hash"]["algorithm"])
self.assertEqual(
"e87351417e4f99504cf0a8868bad03a88b12bc9bda5c36f0ee4c7451e3525f0"
"8146b2130b61cd34b222ee41c38ca75aa9ced9ada65bc3540c80aa66c3d39d689",
"1d6982e325552fd51a9a481252e0c055c32a5b6e3c2d816cf69ca4b2f355a5f7"
"af1abed00ff9d29c08fb32ff293d9d11837ad4372c34675b73682a98b1ec86b0",
craftengine["hash"]["digest"],
)
self.assertEqual(craftengine["hash"]["digest"], craftengine["published_hashes"]["sha512"])
self.assertEqual(
"467fa7317635cf9f76812bd73a47d984c57cf53f",
"179e7b8b191093e41beca56e3d52ae608f46e161",
craftengine["published_hashes"]["sha1"],
)
self.assertEqual(
"https://api.modrinth.com/v2/version/Len451or",
"https://api.modrinth.com/v2/version/aINSQrXC",
craftengine["source_metadata"],
)

Expand Down
2 changes: 1 addition & 1 deletion perf/live/tests/test_run_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_percentile_uses_linear_interpolation(self) -> None:

class LiveInputTest(unittest.TestCase):
def test_uses_latest_paper_compatible_craftengine_by_default(self) -> None:
self.assertEqual("craftengine-paper-26.7.3", DEFAULT_CRAFTENGINE_ARTIFACT)
self.assertEqual("craftengine-paper-26.7.4", DEFAULT_CRAFTENGINE_ARTIFACT)

def test_reserved_ports_are_distinct(self) -> None:
ports = reserve_ports(3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@
import java.lang.reflect.Proxy;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.PluginIdentifiableCommand;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;

public final class MahjongPaperPlugin extends JavaPlugin {
Expand All @@ -58,6 +64,8 @@ public final class MahjongPaperPlugin extends JavaPlugin {
private GameRoomSelectionService gameRoomSelectionService;
private GameRoomSelectionPreviewService gameRoomSelectionPreviewService;
private PluginTask gameRoomTickTask;
private CommandMap legacyPaperCommandMap;
private Command legacyPaperCommand;

@Override
public void onEnable() {
Expand Down Expand Up @@ -153,6 +161,7 @@ public void onEnable() {

@Override
public void onDisable() {
this.unregisterLegacyPaperCommand();
if (this.craftEngine != null) {
this.craftEngine.disableFurnitureInteractionBridge();
this.craftEngine.clearTrackedCullables();
Expand Down Expand Up @@ -226,7 +235,12 @@ private boolean registerMahjongCommand(MahjongCommand mahjongCommand) {

PluginCommand command = this.getCommand("mahjong");
if (command == null) {
this.getLogger().severe("MahjongPaper command is missing from plugin.yml; disabling plugin.");
if (this.registerLegacyPaperCommand(mahjongCommand)) {
return true;
}
this.getLogger().severe(
"MahjongPaper could not register its command through Paper lifecycle events, plugin.yml, or the legacy Paper command map; disabling plugin."
);
this.getServer().getPluginManager().disablePlugin(this);
return false;
}
Expand All @@ -235,6 +249,34 @@ private boolean registerMahjongCommand(MahjongCommand mahjongCommand) {
return true;
}

private boolean registerLegacyPaperCommand(MahjongCommand delegate) {
CommandMap commandMap = this.getServer().getCommandMap();
LegacyPaperMahjongCommand command = new LegacyPaperMahjongCommand(this, delegate);
boolean registeredPrimaryLabel = commandMap.register("mahjong", "mahjongpaper", command);
if (!registeredPrimaryLabel) {
removeLegacyPaperCommand(commandMap, command);
return false;
}
this.legacyPaperCommandMap = commandMap;
this.legacyPaperCommand = command;
return true;
}

private void unregisterLegacyPaperCommand() {
CommandMap commandMap = this.legacyPaperCommandMap;
Command command = this.legacyPaperCommand;
this.legacyPaperCommandMap = null;
this.legacyPaperCommand = null;
if (commandMap != null && command != null) {
removeLegacyPaperCommand(commandMap, command);
}
}

private static void removeLegacyPaperCommand(CommandMap commandMap, Command command) {
commandMap.getKnownCommands().entrySet().removeIf(entry -> entry.getValue() == command);
command.unregister(commandMap);
}

private PaperCommandRegistrationResult registerPaperCommand(MahjongCommand mahjongCommand) {
try {
Class<?> lifecycleEventsClass = Class.forName("io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents");
Expand Down Expand Up @@ -321,6 +363,40 @@ private enum PaperCommandRegistrationResult {
FAILED
}

/** Command-map adapter for Paper versions that support paper-plugin.yml but predate lifecycle commands. */
private static final class LegacyPaperMahjongCommand extends Command implements PluginIdentifiableCommand {
private final MahjongPaperPlugin plugin;
private final MahjongCommand delegate;

private LegacyPaperMahjongCommand(MahjongPaperPlugin plugin, MahjongCommand delegate) {
super(
"mahjong",
"Manage MahjongPaper tables and rounds. Use /mahjong help for command explanations.",
"/mahjong <help|create|botmatch|mode|join|leave|list|spectate|unspectate|table|addbot|removebot|rule|start|state|riichi|tsumo|ron|pon|minkan|chii|kan|skip|kyuushu|settlement|rank|leaderboard|render|inspect|clear|forceend|deletetable|reload>",
java.util.List.of()
);
this.plugin = plugin;
this.delegate = delegate;
this.setPermission(delegate.permission());
}

@Override
public boolean execute(CommandSender sender, String commandLabel, String[] args) {
return this.delegate.onCommand(sender, this, commandLabel, args);
}

@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args) {
List<String> suggestions = this.delegate.onTabComplete(sender, this, alias, args);
return suggestions == null ? List.of() : suggestions;
}

@Override
public Plugin getPlugin() {
return this.plugin;
}
}

public String reloadMahjongConfiguration() {
PluginSettings previousSettings = this.settings;
PluginSettings reloadedSettings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public Component createStateSummary(Player player) {
Locale locale = this.session.plugin().messages().resolveLocale(player);
UUID viewerId = player.getUniqueId();
PlayerActionSnapshot actionSnapshot = this.actionSnapshotFactory.capture(viewerId);
ViewerSummarySnapshot summary = this.captureViewerSummarySnapshot(locale, viewerId, actionSnapshot, false);
ViewerSummarySnapshot summary = this.captureViewerSummarySnapshot(locale, viewerId, actionSnapshot, true);
return this.session.plugin().messages().render(
player,
"command.rule_summary",
Expand Down
Loading