Skip to content

Conversation

dblnz
Copy link
Contributor

@dblnz dblnz commented Aug 31, 2025

Description

This PR closes #723, #704 and partially addresses #318.
These changes modify the way we perform guest tracing to use the tracing crate and its macros (instrument, trace).

How it works

Guest

What makes this possible is the implementation of the Subscriber trait in the hyperlight-guest-tracing crate. By implementing it, we can now handle the capturing of spans and events and choose how to store them and when to export them to the host.

The GuestSubscriber type that implements Subscriber keeps an internal TraceState that holds all the needed information.
Whenever a new span is created, entered or exited, a callback on the subscriber is called so that we can handle the functionality. The same happens with the events also.

Each time a new span or event is added to the internal state, we check whether the buffer got full and send them to the host to process.

Host

When the host detects a VM exit from the guest, it checks whether it contains tracing information in the OutB instruction.
When tracing information is found, the host starts going through it and check against the local storage of spans.

  • If the spans have previously been created, just update the end timestamp (if present) and add new events (if any).
  • If they haven't been created, create and store them.

The spans parents are set based on the information got from the host.

TODO

  • The current issue is with the guest calls that end up calling back into the host.
    These do not correctly set the parents of the spans created in the host to the last one created in the guest before doing the VM exit
    I need to find a way to propagate the context into the guest and back whenever it is needed. But using the Opentelemetry propagators needs std support which we do not have in the guest.
  • There is a corner case with calculating the timestamp for the span that is open when a VM exit is done
Jaeger picture of a Guest call that calls back into the host image

@dblnz dblnz added the kind/enhancement For PRs adding features, improving functionality, docs, tests, etc. label Aug 31, 2025
@dblnz dblnz changed the title Tracing improvements Guest tracing improvements to use tracing crate Aug 31, 2025
@dblnz dblnz marked this pull request as draft August 31, 2025 13:48
Copy link
Contributor

@jprendes jprendes left a comment

Choose a reason for hiding this comment

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

I made a high level review, and what I've seen looks good :-)

@@ -1094,15 +1095,9 @@ impl Hypervisor for HypervWindowsDriver {
}

#[cfg(feature = "trace_guest")]
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
fn read_trace_reg(&self) -> Result<X86_64Regs> {
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be read_regs(&self)?

}
#[cfg(feature = "trace_guest")]
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
fn trace_info_mut(&mut self) -> &mut TraceInfo {
Copy link
Contributor

Choose a reason for hiding this comment

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

<rant>
Ah, I never understand this pattern, why isn't trace_info directly a public member?
But we have that all over the codebase, so... it's ok here
</rant>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because in this case, I need to retrieve the TraceInfo through the &dyn Hypervisor which doesn't know about the KvmDriver's member field.

I don't like it either, to be honest, I think this will look a lot better when we unify the Hypervisors into a single struct. A lot of duplicated code will go also

@@ -11,7 +11,6 @@ members = [
"src/hyperlight_host",
"src/hyperlight_guest_capi",
"src/hyperlight_guest_tracing",
"src/hyperlight_guest_tracing_macro",
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we keep it around, but as deprecated, shimming the new API? so that we don't break anyone who was already using this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think this is the way to go. But I don't think we'll ever use it again

Comment on lines 117 to 118
#[cfg(feature = "trace_guest")]
104 => Ok(OutBAction::TraceRecord),
Copy link
Contributor

Choose a reason for hiding this comment

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

I haven't though about this before, but, does this mean that a guest compiled with an older hyperlight_guest doesn't run in a newer hyperlight_host?
We should think about this more carefully, not particularly in this PR, but I think in general we haven't been too diligent on paying attention to that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've created #845 to track that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, and in this case, Ideally we should keep that 104 value reserved

HyperlightExit::Halt()
| HyperlightExit::IoOut(_, _, _, _)
| HyperlightExit::Mmio(_) => {
// If the result is not a halt, io out, mmio or debug exit, we need to process the trace batch
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the comment wrong? the logic wrong? or my understanding wrong?

Suggested change
// If the result is not a halt, io out, mmio or debug exit, we need to process the trace batch
// If the result is a halt, io out, mmio or debug exit, we need to process the trace batch

HyperlightExit::Halt()
| HyperlightExit::IoOut(_, _, _, _)
| HyperlightExit::Mmio(_) => {
// If the result is not a halt, io out, mmio or debug exit, we need to process the trace batch
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

HyperlightExit::Halt()
| HyperlightExit::IoOut(_, _, _, _)
| HyperlightExit::Mmio(_) => {
// If the result is not a halt, io out, mmio or debug exit, we need to process the trace batch
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

- This feature is not used separate from the mem_profile
- All the unwind logic is now gated by mem_profile

Signed-off-by: Doru Blânzeanu <[email protected]>
- The guest side does not use this type of OutBAction
- The stack unwinding is done either way when the mem_profile feature is enabled

Signed-off-by: Doru Blânzeanu <[email protected]>
@dblnz dblnz force-pushed the tracing-improvements branch 2 times, most recently from d4327a8 to ccaa14e Compare September 10, 2025 22:45
@dblnz dblnz marked this pull request as ready for review September 10, 2025 22:45
@dblnz dblnz force-pushed the tracing-improvements branch from ccaa14e to cceb069 Compare September 10, 2025 22:47
Copy link
Contributor

@ludfjig ludfjig left a comment

Choose a reason for hiding this comment

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

First round review looks good to me. I am curious what the performance looks like with tracing vs without, maybe we could add a benchmark or something for this?

Also, is there any possibility that we don't flush spans/records after exiting the guest, and that some end up not being emitted?

Another thing to consider is log crate vs tracing crate. Should we ditch one? Or is there some mechanism that allows regular logs to be consumed by tracing crate: And should we expose any of these in guest C-api?

Also is the tracing buffer sizes configurable? Maybe it should be if it isn't, so users can tweak it in case it affects performance.

@@ -26,7 +26,6 @@ spin = "0.10.0"
default = ["tracing"]
fuzzing = ["dep:arbitrary"]
trace_guest = []
unwind_guest = []
Copy link
Contributor

Choose a reason for hiding this comment

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

What happened to this feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I merged it into the mem_profile one as it is only used there and it increases the number of combinations we need to test in CI


/// Halt the execution of the guest and returns control to the host.
#[inline(never)]
#[hyperlight_guest_tracing::trace_function]
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we have parent = Span::current() in host as well, but I was wondering if we can leave this out if it's already the default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could do that

Comment on lines +202 to +218
#[cfg(feature = "trace_guest")]
if hyperlight_guest_tracing::is_trace_enabled() {
// If the "trace_guest" feature is enabled and tracing is initialized, log using tracing
tracing::trace!(
event = message,
level = ?log_level,
code.filepath = source,
caller = caller,
source_file = source_file,
code.lineno = line,
);
} else {
send_to_host();
}
#[cfg(not(feature = "trace_guest"))]
{
send_to_host();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this logic correct? Would there be any differences in logging when tracing is enabled and using trace! vs disabled?

Copy link
Contributor Author

@dblnz dblnz Sep 12, 2025

Choose a reason for hiding this comment

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

Yes.
So the current mechanism we have is doing a VM Exit for each log we emit on the guest so that the host receives the info and does the emitting on behalf of the guest. We do not want those kind of interruptions if we can batch the traces, right?

Comment on lines +42 to +45
/// # Safety
/// This function uses unsafe assembly instructions but is safe to call.
/// However, the resulting timestamp is only meaningful if invariant TSC is supported.
pub fn read_tsc() -> u64 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we don't need this safety comment if it's safe?

@jsturtevant
Copy link
Contributor

I am curious what the performance looks like with tracing vs without, maybe we could add a benchmark or something for this?

+1

@dblnz
Copy link
Contributor Author

dblnz commented Sep 12, 2025

First round review looks good to me. I am curious what the performance looks like with tracing vs without, maybe we could add a benchmark or something for this?

Ok, I can do that.

Also, is there any possibility that we don't flush spans/records after exiting the guest, and that some end up not being emitted?

Hmm, in my limited testing I haven't seen this case, but I wouldn't exclude the possibility.
One scenario that comes to mind is cancellation, where the vCPU forced to stop, so no out instruction to deliver the info.
Any ideas how we can treat these scenarios?

Another thing to consider is log crate vs tracing crate. Should we ditch one? Or is there some mechanism that allows regular logs to be consumed by tracing crate: And should we expose any of these in guest C-api?

I am not sure about the best approach is.
The solution I added for guest spans/events only works with opentelemetry Subscribers on the host, so if no opentelemetry subscribers, the they won't be captured (haven't tried it yet, but this is how it should work)

Also is the tracing buffer sizes configurable? Maybe it should be if it isn't, so users can tweak it in case it affects performance.

The tracing buffer is compile time configurable, which I agree is not ideal for customers.
But I needed a fixed size buffer so that I could reliably give the pointer to the host to access memory (current approach, I don't know if it is the best, but it is certainly faster than using the input/output buffer which copies data twice. This way we only copy once, on the host).

- This helps with keeping code separate and easily gating it out

Signed-off-by: Doru Blânzeanu <[email protected]>
@dblnz dblnz force-pushed the tracing-improvements branch from cceb069 to b55e36c Compare September 12, 2025 15:59
- This steps cleans up codebase for the new way of tracing guests
- The current method involves custom macros and logic that are not
the best for maintainability

Signed-off-by: Doru Blânzeanu <[email protected]>
- Define a separate struct that holds the functionality related to
  memory profiling of the guest

Signed-off-by: Doru Blânzeanu <[email protected]>
- Rename TraceInfo to reflect only being used by mem_profile

Signed-off-by: Doru Blânzeanu <[email protected]>
- Adds a type that implements the Subscriber trait of the tracing_core
crate that allows the type to be set as the global Subscriber of
the crate
- This way we can handle the adding of new spans and events
and store them where/how we want

Signed-off-by: Doru Blânzeanu <[email protected]>
@dblnz dblnz force-pushed the tracing-improvements branch from b55e36c to 186a34d Compare September 12, 2025 16:20
- implement add_span and event methods that store the info and report
it to the host when the buffer gets full

Signed-off-by: Doru Blânzeanu <[email protected]>
- Parse the spans and events coming from the guest and
create corresponding spans and events from the host that
mimics a single call from host
- Create a `TraceContext` that handles a call into a guest

Signed-off-by: Doru Blânzeanu <[email protected]>
- conditionally handle logs either through tracing or the dedicated VM exit
  based on whether tracing is initialized on the guest

Signed-off-by: Doru Blânzeanu <[email protected]>
Signed-off-by: Doru Blânzeanu <[email protected]>
@dblnz dblnz force-pushed the tracing-improvements branch from 186a34d to d527f19 Compare September 12, 2025 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement For PRs adding features, improving functionality, docs, tests, etc.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve guest tracing
4 participants