Skip to content

Commit ea25c90

Browse files
authored
Debug stepping feature (#178)
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
1 parent 940c057 commit ea25c90

File tree

8 files changed

+711
-8
lines changed

8 files changed

+711
-8
lines changed

src/debug.rs

Lines changed: 511 additions & 0 deletions
Large diffs are not rendered by default.

src/flush.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ use crate::{
3838
#[cfg(feature = "single_threaded_async")]
3939
use crate::async_execution::SingleThreadedExecution;
4040

41+
#[cfg(feature = "trace")]
42+
use crate::{Debug, DebugRoster};
43+
4144
#[derive(Resource, Default, Clone, Copy)]
4245
pub struct FlushParameters {
4346
/// By default, a flush will loop until the whole [`OperationRoster`] is empty.
@@ -87,11 +90,29 @@ pub fn flush_execution() -> ScheduleConfigs<ScheduleSystem> {
8790
fn flush_execution_impl(
8891
world: &mut World,
8992
new_service_query: &mut QueryState<(Entity, &mut ServiceHook), Added<ServiceHook>>,
93+
#[cfg(feature = "trace")] debug: &mut SystemState<Option<Res<Debug>>>,
9094
) {
9195
let parameters = *world.get_resource_or_insert_with(FlushParameters::default);
9296
let mut roster = OperationRoster::new();
9397
collect_from_channels(&parameters, new_service_query, world, &mut roster);
9498

99+
#[cfg(feature = "trace")]
100+
{
101+
let debug = debug.get(world);
102+
if let Some(debug) = debug
103+
&& debug.is_changed()
104+
{
105+
world.get_resource_or_init::<DebugRoster>();
106+
world.resource_scope::<DebugRoster, _>(|world, mut debug_roster| {
107+
debug_roster.release_unpaused(world, &mut roster);
108+
});
109+
}
110+
111+
// Queue any operations that needed to be deferred
112+
let mut deferred = world.get_resource_or_insert_with(DeferredRoster::default);
113+
roster.append(&mut deferred);
114+
}
115+
95116
let mut loop_count = 0;
96117
while !roster.is_empty() {
97118
for e in roster.deferred_despawn.drain(..) {

src/input.rs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ use crate::{
3535
};
3636

3737
#[cfg(feature = "trace")]
38-
use crate::{MessageSent, Trace, TraceToggle, TracedEvent, UniversalTraceToggle};
38+
use crate::{
39+
Debug, DebugRoster, MessageSent, Trace, TraceToggle, TracedEvent, UniversalTraceToggle,
40+
};
3941

4042
pub type Seq = u32;
4143

@@ -486,9 +488,66 @@ impl ManageInput for World {
486488
&mut self,
487489
source: Entity,
488490
) -> Result<Option<Input<T>>, OperationError> {
489-
let mut storage = self.get_mut::<InputStorage<T>>(source).or_broken()?;
490-
let input = storage.reverse_queue.pop();
491-
Ok(input)
491+
#[cfg(not(feature = "trace"))]
492+
{
493+
let mut storage = self.get_mut::<InputStorage<T>>(source).or_broken()?;
494+
let input = storage.reverse_queue.pop();
495+
return Ok(input);
496+
}
497+
498+
#[cfg(feature = "trace")]
499+
{
500+
self.get_resource_or_init::<Debug>();
501+
self.resource_scope::<Debug, _>(|world, mut debug| {
502+
if !debug.is_active() {
503+
// Revert to the usual implementation of popping the next
504+
let mut storage = world.get_mut::<InputStorage<T>>(source).or_broken()?;
505+
let input = storage.reverse_queue.pop();
506+
return Ok(input);
507+
} else {
508+
world.get_resource_or_init::<DebugRoster>();
509+
world.resource_scope::<DebugRoster, _>(|world, mut debug_roster| {
510+
let storage = world.get::<InputStorage<T>>(source).or_broken()?;
511+
let rev_next = storage.reverse_queue.iter().rev().position(|input| {
512+
let session = input.session;
513+
let seq = input.seq;
514+
515+
// Evaluate whether we have hit a breakpoint. This will
516+
// pause the current session if we have.
517+
debug.evaluate_break(session, source, world);
518+
519+
let mut is_paused = debug.is_paused(session, world);
520+
if is_paused {
521+
// We need to track this request inside the debug roster
522+
if debug_roster.is_allowed(RequestId {
523+
session,
524+
source,
525+
seq,
526+
}) {
527+
// If this input has been given permission to
528+
// to be taken, change is_paused to false so
529+
// it will be passed along.
530+
is_paused = false;
531+
}
532+
}
533+
534+
!is_paused
535+
});
536+
537+
debug.notify_session_changes(world);
538+
539+
if let Some(rev_next) = rev_next {
540+
let mut storage =
541+
world.get_mut::<InputStorage<T>>(source).or_broken()?;
542+
let next = storage.reverse_queue.len() - rev_next - 1;
543+
return Ok(Some(storage.reverse_queue.remove(next)));
544+
} else {
545+
return Ok(None);
546+
}
547+
})
548+
}
549+
})
550+
}
492551
}
493552

494553
fn cleanup_inputs<T: 'static + Send + Sync>(

src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ pub use chain::*;
9393
pub mod channel;
9494
pub use channel::*;
9595

96+
#[cfg(feature = "trace")]
97+
pub mod debug;
98+
#[cfg(feature = "trace")]
99+
pub use debug::*;
100+
96101
#[cfg(feature = "diagram")]
97102
pub mod diagram;
98103
#[cfg(feature = "diagram")]
@@ -515,6 +520,9 @@ pub mod prelude {
515520
},
516521
};
517522

523+
#[cfg(feature = "trace")]
524+
pub use crate::{Debug, DebugStepExt, UniversalTraceToggle};
525+
518526
pub use futures::FutureExt;
519527
}
520528

src/operation.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ impl OperationType {
220220
}
221221
}
222222

223-
#[derive(Default)]
223+
#[derive(Default, Debug)]
224224
pub struct OperationRoster {
225225
/// Operation sources that should be triggered
226226
pub(crate) queue: VecDeque<Entity>,
@@ -364,7 +364,7 @@ impl OperationError {
364364
}
365365
}
366366

367-
pub type OperationResult = Result<(), OperationError>;
367+
pub type OperationResult<T = ()> = Result<T, OperationError>;
368368
pub type ReachabilityResult = Result<bool, OperationError>;
369369

370370
pub struct OperationSetup<'a> {

src/operation/join.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,32 @@ mod tests {
136136

137137
let mut context = TestingContext::minimal_plugins();
138138

139+
#[cfg(feature = "trace")]
140+
use {
141+
crate::{TracedEvent, TracedEventKind},
142+
bevy_ecs::prelude::{Resource, Trigger},
143+
};
144+
145+
#[cfg(feature = "trace")]
146+
#[derive(Debug, Resource, Default)]
147+
struct TraceLog {
148+
events: Vec<TracedEventKind>,
149+
}
150+
151+
#[cfg(feature = "trace")]
152+
{
153+
context
154+
.app
155+
.world_mut()
156+
.get_resource_or_insert_with(UniversalTraceToggle::on);
157+
context.app.world_mut().add_observer(
158+
|trace: Trigger<TracedEvent>, world: &mut World| {
159+
let mut log = world.get_resource_or_init::<TraceLog>();
160+
log.events.push(trace.event().event.clone());
161+
},
162+
);
163+
}
164+
139165
let workflow = context.spawn_io_workflow(|scope, builder| {
140166
let node = builder.create_map(|input: Async<f64, StreamOf<f64>>| async move {
141167
input.streams.send(input.request);
@@ -158,7 +184,20 @@ mod tests {
158184
.connect(scope.terminate);
159185
});
160186

161-
let r = context.resolve_request(2.0, workflow);
187+
let r = match context.try_resolve_request(2.0, workflow, Duration::from_secs(2)) {
188+
Ok(ok) => ok,
189+
Err(err) => {
190+
#[cfg(feature = "trace")]
191+
{
192+
println!(
193+
"Activity trace of test failure:\n{:#?}",
194+
context.app.world().resource::<TraceLog>(),
195+
);
196+
}
197+
assert!(false, "failed to resolve:\n{err}");
198+
return;
199+
}
200+
};
162201
assert_eq!(r, 4.0);
163202
}
164203

src/session.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*
1616
*/
1717

18-
use bevy_ecs::prelude::{Component, Entity, World};
18+
use bevy_ecs::prelude::{ChildOf, Component, Entity, World};
1919

2020
use crate::{Cancellation, ScopedSessionBundle, Seq};
2121

@@ -69,7 +69,14 @@ pub trait ManageSession {
6969
seq: Seq,
7070
) -> Entity;
7171

72+
/// Despawn a session
7273
fn despawn_session(&mut self, entity: Entity);
74+
75+
/// Returns true if `descendent_session` is a descendent of `parent_session`.
76+
/// Note that this also returns true if `parent_session == descendent_session`,
77+
/// so this is meant to be used when you're trying to figure out of a session
78+
/// exists anywhere inside another session, inclusively.
79+
fn is_descendent_session(&self, parent_session: Entity, descendent_session: Entity) -> bool;
7380
}
7481

7582
impl ManageSession for World {
@@ -128,6 +135,26 @@ impl ManageSession for World {
128135

129136
self.despawn(session);
130137
}
138+
139+
fn is_descendent_session(
140+
&self,
141+
parent_session: Entity,
142+
mut descendent_session: Entity,
143+
) -> bool {
144+
if parent_session == descendent_session {
145+
return true;
146+
}
147+
148+
while let Some(parent) = self.get::<ChildOf>(descendent_session).map(|c| c.parent()) {
149+
if parent == parent_session {
150+
return true;
151+
}
152+
153+
descendent_session = parent;
154+
}
155+
156+
false
157+
}
131158
}
132159

133160
#[cfg(feature = "trace")]

src/trace.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,36 @@ impl SessionEvent {
486486
world.trigger(TracedEvent::now(event));
487487
}
488488

489+
pub(crate) fn paused_by_user(session: Entity, world: &mut World) {
490+
let session_stack = get_session_stack_from_world(session, world);
491+
let event = SessionEvent {
492+
session_stack,
493+
change: SessionChange::Paused(PauseCause::UserRequest),
494+
};
495+
496+
world.trigger(TracedEvent::now(event));
497+
}
498+
499+
pub(crate) fn paused_by_breakpoint(session: Entity, breakpoint: Entity, world: &mut World) {
500+
let session_stack = get_session_stack_from_world(session, world);
501+
let event = SessionEvent {
502+
session_stack,
503+
change: SessionChange::Paused(PauseCause::Breakpoint(breakpoint)),
504+
};
505+
506+
world.trigger(TracedEvent::now(event));
507+
}
508+
509+
pub(crate) fn unpaused(session: Entity, world: &mut World) {
510+
let session_stack = get_session_stack_from_world(session, world);
511+
let event = SessionEvent {
512+
session_stack,
513+
change: SessionChange::Unpaused,
514+
};
515+
516+
world.trigger(TracedEvent::now(event));
517+
}
518+
489519
pub(crate) fn cleanup(session: Entity, world: &mut World) {
490520
let session_stack = get_session_stack_from_world(session, world);
491521
let event = SessionEvent {
@@ -513,6 +543,14 @@ pub enum SessionChange {
513543
},
514544
BeginCleanup,
515545
Despawned,
546+
Paused(PauseCause),
547+
Unpaused,
548+
}
549+
550+
#[derive(Debug, Clone)]
551+
pub enum PauseCause {
552+
UserRequest,
553+
Breakpoint(Entity),
516554
}
517555

518556
#[derive(Debug, Clone)]

0 commit comments

Comments
 (0)