mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
feat(pumpkin): add /debug start and stop commands (#2782)
Implements the vanilla /debug start and /debug stop commands (tracked in #15): - /debug start opens one server-wide tick profiling session and rejects duplicate starts. - /debug stop reports elapsed seconds, processed ticks, and average TPS, then returns the floored average TPS. - Profiling state handles tick counter wraparound and can be restarted after a completed session. - Registers minecraft:command.debug at operator permission level 3 and uses the existing Java and Bedrock translation keys. /debug function is intentionally out of scope because Pumpkin does not yet have the required function runtime.
This commit is contained in:
112
crates/pumpkin/src/command/commands/debug.rs
Normal file
112
crates/pumpkin/src/command/commands/debug.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use pumpkin_data::translation;
|
||||
use pumpkin_util::PermissionLvl;
|
||||
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
|
||||
use pumpkin_util::text::TextComponent;
|
||||
|
||||
use crate::command::CommandSender;
|
||||
use crate::command::argument_builder::{ArgumentBuilder, command, literal};
|
||||
use crate::command::context::command_context::CommandContext;
|
||||
use crate::command::errors::error_types::CommandErrorType;
|
||||
use crate::command::node::dispatcher::CommandDispatcher;
|
||||
use crate::command::node::{CommandExecutor, CommandExecutorResult};
|
||||
use crate::server::debug_profiler::{StartDebugProfileError, StopDebugProfileError};
|
||||
|
||||
const DESCRIPTION: &str = "Starts or stops a tick profiling session.";
|
||||
const PERMISSION: &str = "minecraft:command.debug";
|
||||
|
||||
const ALREADY_RUNNING_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DEBUG_ALREADYRUNNING,
|
||||
translation::java::COMMANDS_DEBUG_ALREADYRUNNING,
|
||||
);
|
||||
|
||||
const NOT_RUNNING_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
|
||||
translation::java::COMMANDS_DEBUG_NOTRUNNING,
|
||||
translation::bedrock::COMMANDS_DEBUG_NOTSTARTED,
|
||||
);
|
||||
|
||||
struct DebugStartExecutor;
|
||||
|
||||
impl CommandExecutor for DebugStartExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let server = context.server();
|
||||
let current_tick = server.tick_count.load(Ordering::Relaxed);
|
||||
server.debug_profiler.start(current_tick).map_err(
|
||||
|StartDebugProfileError::AlreadyRunning| {
|
||||
ALREADY_RUNNING_ERROR_TYPE.create_without_context()
|
||||
},
|
||||
)?;
|
||||
|
||||
context
|
||||
.source
|
||||
.send_feedback(
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DEBUG_STARTED,
|
||||
translation::bedrock::COMMANDS_DEBUG_START,
|
||||
[],
|
||||
),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugStopExecutor;
|
||||
|
||||
impl CommandExecutor for DebugStopExecutor {
|
||||
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
|
||||
Box::pin(async move {
|
||||
let server = context.server();
|
||||
let current_tick = server.tick_count.load(Ordering::Relaxed);
|
||||
let result = server.debug_profiler.stop(current_tick).map_err(
|
||||
|StopDebugProfileError::NotRunning| NOT_RUNNING_ERROR_TYPE.create_without_context(),
|
||||
)?;
|
||||
|
||||
let seconds = result.duration.as_secs_f64();
|
||||
let tps = result.ticks_per_second();
|
||||
let arguments = [
|
||||
TextComponent::text(format!("{seconds:.2}")),
|
||||
TextComponent::text(result.ticks.to_string()),
|
||||
TextComponent::text(format!("{tps:.2}")),
|
||||
];
|
||||
let feedback = if matches!(context.source.output, CommandSender::Player(_)) {
|
||||
TextComponent::translate_cross(
|
||||
translation::java::COMMANDS_DEBUG_STOPPED,
|
||||
translation::bedrock::COMMANDS_DEBUG_STOP,
|
||||
arguments,
|
||||
)
|
||||
} else {
|
||||
// Bedrock's debug-stop translation uses printf-style placeholders that the
|
||||
// server-side console renderer cannot resolve. Non-player command sources need
|
||||
// an already-rendered message; players still receive their native translation.
|
||||
TextComponent::text(format!(
|
||||
"Stopped tick profiling after {seconds:.2} second(s) and {} tick(s) ({tps:.2} tick(s) per second)",
|
||||
result.ticks
|
||||
))
|
||||
};
|
||||
context.source.send_feedback(feedback, true).await;
|
||||
|
||||
Ok(result.command_result())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
|
||||
registry.register_permission_or_panic(Permission::new(
|
||||
PERMISSION,
|
||||
DESCRIPTION,
|
||||
PermissionDefault::Op(PermissionLvl::Three),
|
||||
));
|
||||
|
||||
dispatcher.register(
|
||||
command("debug", DESCRIPTION)
|
||||
.requires(PERMISSION)
|
||||
.then(literal("start").executes(DebugStartExecutor))
|
||||
.then(literal("stop").executes(DebugStopExecutor)),
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ mod clear;
|
||||
mod clone;
|
||||
mod damage;
|
||||
mod data;
|
||||
mod debug;
|
||||
pub mod defaultgamemode;
|
||||
mod deop;
|
||||
mod dialog;
|
||||
@@ -177,6 +178,7 @@ pub fn default_dispatcher(
|
||||
say::register(&mut dispatcher, registry);
|
||||
banlist::register(&mut dispatcher, registry);
|
||||
difficulty::register(&mut dispatcher, registry);
|
||||
debug::register(&mut dispatcher, registry);
|
||||
dialog::register(&mut dispatcher, registry);
|
||||
execute::register(&mut dispatcher, registry);
|
||||
fillbiome::register(&mut dispatcher, registry);
|
||||
|
||||
141
crates/pumpkin/src/server/debug_profiler.rs
Normal file
141
crates/pumpkin/src/server/debug_profiler.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct DebugProfileSession {
|
||||
started_at: Instant,
|
||||
started_tick: u32,
|
||||
}
|
||||
|
||||
/// Measurements collected by a completed `/debug start` profiling session.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DebugProfileResult {
|
||||
pub duration: Duration,
|
||||
pub ticks: u32,
|
||||
}
|
||||
|
||||
impl DebugProfileResult {
|
||||
#[must_use]
|
||||
pub fn ticks_per_second(self) -> f64 {
|
||||
let seconds = self.duration.as_secs_f64();
|
||||
if seconds == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
f64::from(self.ticks) / seconds
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn command_result(self) -> i32 {
|
||||
let floored_tps = self
|
||||
.ticks_per_second()
|
||||
.floor()
|
||||
.clamp(0.0, f64::from(i32::MAX));
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
{
|
||||
floored_tps as i32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StartDebugProfileError {
|
||||
AlreadyRunning,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StopDebugProfileError {
|
||||
NotRunning,
|
||||
}
|
||||
|
||||
/// Owns the single server-wide tick profiling session used by `/debug`.
|
||||
#[derive(Default)]
|
||||
pub struct DebugProfiler {
|
||||
active_session: Mutex<Option<DebugProfileSession>>,
|
||||
}
|
||||
|
||||
impl DebugProfiler {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn start(&self, current_tick: i32) -> Result<(), StartDebugProfileError> {
|
||||
self.start_at(current_tick as u32, Instant::now())
|
||||
}
|
||||
|
||||
pub fn stop(&self, current_tick: i32) -> Result<DebugProfileResult, StopDebugProfileError> {
|
||||
self.stop_at(current_tick as u32, Instant::now())
|
||||
}
|
||||
|
||||
fn start_at(&self, current_tick: u32, now: Instant) -> Result<(), StartDebugProfileError> {
|
||||
let mut active_session = self
|
||||
.active_session
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
||||
if active_session.is_some() {
|
||||
return Err(StartDebugProfileError::AlreadyRunning);
|
||||
}
|
||||
|
||||
*active_session = Some(DebugProfileSession {
|
||||
started_at: now,
|
||||
started_tick: current_tick,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_at(
|
||||
&self,
|
||||
current_tick: u32,
|
||||
now: Instant,
|
||||
) -> Result<DebugProfileResult, StopDebugProfileError> {
|
||||
let session = self
|
||||
.active_session
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.take()
|
||||
.ok_or(StopDebugProfileError::NotRunning)?;
|
||||
|
||||
Ok(DebugProfileResult {
|
||||
duration: now.saturating_duration_since(session.started_at),
|
||||
ticks: current_tick.wrapping_sub(session.started_tick),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DebugProfiler, StartDebugProfileError, StopDebugProfileError};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn profile_lifecycle_enforces_state_and_reports_measurements() {
|
||||
let profiler = DebugProfiler::new();
|
||||
let start = Instant::now();
|
||||
|
||||
assert_eq!(
|
||||
profiler.stop_at(42, start),
|
||||
Err(StopDebugProfileError::NotRunning)
|
||||
);
|
||||
assert_eq!(profiler.start_at(42, start), Ok(()));
|
||||
assert_eq!(
|
||||
profiler.start_at(100, start + Duration::from_secs(1)),
|
||||
Err(StartDebugProfileError::AlreadyRunning)
|
||||
);
|
||||
|
||||
let result = profiler
|
||||
.stop_at(62, start + Duration::from_secs(2))
|
||||
.expect("the running profile should stop");
|
||||
|
||||
assert_eq!(result.duration, Duration::from_secs(2));
|
||||
assert_eq!(result.ticks, 20);
|
||||
assert_eq!(result.ticks_per_second(), 10.0);
|
||||
assert_eq!(result.command_result(), 10);
|
||||
assert_eq!(
|
||||
profiler.stop_at(62, start + Duration::from_secs(2)),
|
||||
Err(StopDebugProfileError::NotRunning)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ use tokio::task::{JoinHandle, JoinSet};
|
||||
use tokio_util::task::TaskTracker;
|
||||
|
||||
mod connection_cache;
|
||||
pub(crate) mod debug_profiler;
|
||||
pub mod enchantment;
|
||||
mod key_store;
|
||||
pub mod recipe;
|
||||
@@ -130,6 +131,8 @@ pub struct Server {
|
||||
pub aggregated_tick_times_nanos: AtomicI64,
|
||||
/// Total number of ticks processed by the server
|
||||
pub tick_count: AtomicI32,
|
||||
/// Owns the server-wide tick profiling session used by `/debug`.
|
||||
pub(crate) debug_profiler: debug_profiler::DebugProfiler,
|
||||
/// Random unique Server ID used by Bedrock Edition
|
||||
pub server_guid: u64,
|
||||
/// Player idle timeout in minutes (0 = disabled)
|
||||
@@ -284,6 +287,7 @@ impl Server {
|
||||
tick_times_nanos: Mutex::new([0; 100]),
|
||||
aggregated_tick_times_nanos: AtomicI64::new(0),
|
||||
tick_count: AtomicI32::new(0),
|
||||
debug_profiler: debug_profiler::DebugProfiler::new(),
|
||||
tasks: TaskTracker::new(),
|
||||
runtime: tokio::runtime::Handle::current(),
|
||||
task_scheduler: Arc::new(TaskScheduler::new()),
|
||||
|
||||
Reference in New Issue
Block a user