Debug logging, metrics, error codes, and performance tips
Debugging
Tools and techniques for diagnosing issues with the bridge.
Debug Logging
Enabling
Toggle debug mode at runtime:
- In-game:
/bridge debug - Console:
/bridge debug
Debug state is per-server (global toggle), not per-script.
Log format
Debug messages use the [PJB] prefix with direction indicators:
[PJB] P2J script.py 2.3ms call getHealth [42] → {{"health": 20.0}}
[PJB] J2P script.py 0.1ms event PlayerMoveEvent {{player: "Steve", ...}}Format: [PJB] {{direction}} {{script}} {{time}} {{type}} {{summary}}
| Field | Values |
|---|---|
| Direction | P2J (Python→Java), J2P (Java→Python) |
| Script | Script filename |
| Time | Round-trip or processing time in milliseconds |
| Type | call, event, release, register_command, etc. |
| Summary | Truncated message content |
Message summarization
Large messages are automatically summarized:
- Method calls show: method name + handle + truncated args
- Events show: event name + key fields
- Responses show: truncated return value
- Release batches show: handle count
Metrics
The Python-side MetricsFacade exposes server performance data:
from bridge import server
tps = server.tps # Ticks per second (target: 20.0)
mspt = server.mspt # Milliseconds per tick
tick_time = server.last_tick # Last tick time in ms
queue = server.queue_length # Pending calls in main thread queue
Metric sources
| Metric | Source | Thread-safe |
|---|---|---|
tps | Bukkit.getTPS()[0] | Yes |
mspt | MinecraftServer.getAverageTickTimeNanos() | Yes |
last_tick | Bridge-internal counter | Yes |
queue_length | Main thread queue .size() | Yes |
These are all read-only and can be safely accessed from async handlers.
Error Codes
ENTITY_GONE
BridgeError: ENTITY_GONE - Entity no longer existsCause: The proxy refers to an entity that has been removed from the world (died, despawned, unloaded).
Fix: Check entity.is_valid() before operating, or catch BridgeError:
try:
entity.set_health(20)
except BridgeError:
pass # Entity was removed
ATOMIC_ABORT
BridgeError: ATOMIC_ABORT - Atomic batch failedCause: An operation inside an atomic() block raised an exception, causing the entire batch to abort.
Fix: Ensure all operations in the atomic block are valid:
async with atomic():
# Every operation here must succeed
player.set_health(20)
player.set_food_level(20)TIMEOUT
BridgeError: TIMEOUT - Call did not complete within timeoutCause: The Java side didn't respond within the timeout period. Usually means the main thread is blocked or overloaded.
Fix: Check server TPS. If the server is lagging, calls will queue up.
Generic BridgeError
BridgeError: {{message}}Cause: Java-side exception during method invocation. The exception message is forwarded.
Common causes:
NullPointerException— calling methods on null resultsIllegalArgumentException— wrong argument typesIllegalStateException— calling Bukkit API from wrong thread
Performance Tips
Use batching for related operations
# Bad: 20 round-trips
for i in range(20):
player.get_inventory().set_item(i, item)
# Good: 1 round-trip with frame batching
async with frame():
for i in range(20):
player.get_inventory().set_item(i, item)Fire-and-forget for void calls
Most setters and void methods (like send_message, set_health, teleport) are fire-and-forget — Python doesn't wait for a response. This makes them nearly instant:
# These are fire-and-forget (instant, no round-trip):
await player.send_message("Hello")
await player.set_health(20)
await player.teleport(location)
# These still need a round-trip (they return data):
health = await player.get_health()
world = await player.get_world()Install msgpack for faster serialization
The bridge supports msgpack as an alternative wire format. Install it in your Python environment for smaller payloads and faster serialization:
pip install msgpackThe bridge auto-detects msgpack on startup and negotiates the format with Java. No code changes needed.
Use thread-safe APIs when possible
Calls that are thread-safe skip main thread scheduling and execute immediately:
# These are thread-safe and fire-and-forget (fastest):
await player.send_message("Hello")
await player.play_sound(...)
await player.kick("reason")
# These need main thread (slower):
world.get_block_at(x, y, z)
entity.get_passengers()Keep event handlers fast
Event handlers block the server's main thread while waiting for your Python code to respond:
@event("player_move")
async def on_move(e):
# BAD: slow operation blocks the server
result = await some_expensive_call()
# GOOD: only do quick checks in move events
if e.player.is_sneaking:
e.cancel()High-frequency events like player_move should return as quickly as possible.
Release references early
# BAD: holds references to all chunks forever
chunks = []
for x in range(10):
for z in range(10):
chunks.append(world.get_chunk_at(x, z))
# GOOD: process and discard
for x in range(10):
for z in range(10):
chunk = world.get_chunk_at(x, z)
process(chunk)
# chunk goes out of scope, handle released on next GC
Use asyncio.gather for independent calls
# Sequential: 3 round-trips
health = await player.get_health()
food = await player.get_food_level()
level = await player.get_level()
# Parallel: 1 round-trip (all sent together)
health, food, level = await asyncio.gather(
player.get_health(),
player.get_food_level(),
player.get_level()
)Monitor with metrics
if server.tps < 18.0:
# Server is struggling, reduce load
skip_expensive_operations()
if server.queue_length > 50:
# Too many pending calls
await asyncio.sleep(0.1) # Back off