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:

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}}

FieldValues
DirectionP2J (Python→Java), J2P (Java→Python)
ScriptScript filename
TimeRound-trip or processing time in milliseconds
Typecall, event, release, register_command, etc.
SummaryTruncated message content

Message summarization

Large messages are automatically summarized:


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

MetricSourceThread-safe
tpsBukkit.getTPS()[0]Yes
msptMinecraftServer.getAverageTickTimeNanos()Yes
last_tickBridge-internal counterYes
queue_lengthMain 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 exists

Cause: 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 failed

Cause: 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 timeout

Cause: 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:


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 msgpack

The 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