Skip to content

Implement hit count breakpoints#1087

Merged
lionel- merged 5 commits intomainfrom
feature/hit-count
Mar 8, 2026
Merged

Implement hit count breakpoints#1087
lionel- merged 5 commits intomainfrom
feature/hit-count

Conversation

@lionel-
Copy link
Copy Markdown
Contributor

@lionel- lionel- commented Mar 5, 2026

Branched from #1086
Addresses posit-dev/positron#12360

Adds support for DAP hit count breakpoints. When a breakpoint has a hitCondition, the breakpoint location must be reached at least that many times before it fires. The value is interpreted as a plain integer with >= semantics. For more complex hit patterns, users can combine hit counts with conditional breakpoints.

Hit counts are reset when ReadConsole returns to a top-level (non-browser) prompt, meaning the execution that may have hit breakpoints is complete. This way counts survive across continue/step within a debug session but start fresh for new sessions.

Hit count integrates with the existing breakpoint feature chain. Hit counts with conditions require both to be satisfied, and hit counts with log messages only emit output once the threshold is reached. If the hit count can't be parsed, the error message is propagated to the Console in breakpoint fences with a clickable link.

QA Notes

Tested on the backend side.

Hit counts should behave well on their own or in combination with logpoints and conditional breakpoints. Parse errors appear in the console.

Screenshot 2026-03-05 at 12 05 36 Screenshot 2026-03-05 at 12 05 24

@lionel- lionel- requested a review from DavisVaughan March 5, 2026 11:46
Copy link
Copy Markdown
Contributor

@DavisVaughan DavisVaughan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think there is an actual bug with the fact that condition should be evaluated before incrementing the hit condition value.

And we should probably think about implementing as a bare number vs a boolean expression, as I mention below

But overall looks quite nice and is something I've definitely wanted in RStudio for a long long time!

Comment thread crates/ark/src/console.rs
// here, but only containing high-level information such as `search()`
// contents and `ls(rho)`.
if !self.debug_is_debugging && !matches!(info.kind, PromptKind::InputRequest) {
self.debug_dap.lock().unwrap().reset();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we do this every time we come through read-console at top level?

Is there any way to only do it after we exit the debugger?

Just curious. I know it's probably not performance sensitive at all right now, I'm just wondering if we can only call this when we really need it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right place, for this purpose we only know a debug session has finished when we come back to a non-debug prompt.

Comment thread crates/ark/src/console_debug.rs Outdated
return Ok(RObject::from(false).sexp);
}

let hit_count = dap.increment_hit_count(&uri, id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a random thought that this function is called ps_should_break and that makes me think its a simple yes/no kind of thing, but man it's doing way more now! I wonder if there is a better name we can come up with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I had the same feeling. It's now handle_breakpoint().

injected: false,
condition: bp.condition.clone(),
log_message: bp.log_message.clone(),
hit_condition: bp.hit_condition.clone(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not be opposed to parsing hit_condition as an integer here so you don't have to do it at each should-break step

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'd be less user friendly because then users don't get parse error messages in a breakpoint block in the Console.

@@ -497,6 +498,8 @@ impl<R: Read, W: Write> DapServer<R, W> {
injected: false,
condition: bp.condition.clone(),
log_message: bp.log_message.clone(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to call out that the docs suggest

The expression that controls how many hits of the breakpoint are ignored.

To me that means if you set hit_condition = 2, then 2 hits should be ignored, and only on the 3rd does it stop.

But you said its a >= implementation, and that seems to go against that?

Copy link
Copy Markdown
Contributor Author

@lionel- lionel- Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adapters generally use >= or == (and some don't allow simple numbers). Since I've opted for requiring numbers, it makes more sense to go with the former. That's also what's most generally useful.

Comment thread crates/ark/src/console_debug.rs
// Must drop before calling back into R to avoid deadlock
drop(dap);

if let Some(ref hit_condition) = hit_condition {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh I believe you have an ordering error!

According to the docs

   * If both this property and `condition` are specified, `hitCondition` should
   * be evaluated only if the `condition` is met, and the debug adapter should
   * stop only if both conditions are met.

So IIUC, this whole section should come after your should_break early exit!

i.e. if my condition is x > 5 and then i have a hitCondition of 2, then when x <= 5 it should not be incrementing the hitCondition!

This would make for a good test!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

drop(dap);

if let Some(ref hit_condition) = hit_condition {
match hit_condition.trim().parse::<u64>() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also want to call out that I'm not sure that interpreting this as a simple number is how most people implement hit conditions

https://github.com/go-delve/delve/pull/2490/changes#diff-d8611b2d0b4b5d3b08ffb7b8e3f913420120a431d6dfd81c6f2a8221065bdf70R225-R238

It looks like a common way to do this is to allow the user to supply a hit condition of == 5, where you fill in the LHS of the binary expression there with the current hitCount.

Then users could supply > 5 as well, for "break every time after 5 hits"

You'd support a limited set of options, like ==, >, >=, <, <=, and that would maybe be enough.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep the agent initially implemented a small parser along these lines, but that seemed a bit overkill to me. I thought we'd start with a simple >= interpretation.

@lionel- lionel- force-pushed the feature/log-breakpoints branch from 1fb6c9b to 48fb3aa Compare March 8, 2026 17:15
@lionel- lionel- force-pushed the feature/hit-count branch from c46455e to 542663f Compare March 8, 2026 20:32
@lionel- lionel- force-pushed the feature/log-breakpoints branch from 04f45bb to 22a9677 Compare March 8, 2026 20:40
Base automatically changed from feature/log-breakpoints to main March 8, 2026 20:40
@lionel- lionel- force-pushed the feature/hit-count branch from 542663f to e559ab2 Compare March 8, 2026 20:41
@lionel- lionel- merged commit 4835a67 into main Mar 8, 2026
9 checks passed
@lionel- lionel- deleted the feature/hit-count branch March 8, 2026 20:42
@github-actions github-actions Bot locked and limited conversation to collaborators Mar 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants