Skip to content

Commit 0c36e87

Browse files
committed
fix(virtq): adjust sizes for benchmarks
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent 15a5aba commit 0c36e87

5 files changed

Lines changed: 105 additions & 72 deletions

File tree

src/hyperlight_common/src/virtq/mod.rs

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -896,14 +896,14 @@ mod tests {
896896
}
897897

898898
#[test]
899-
fn test_reclaim_then_poll_preserves_order() {
899+
fn test_reclaim_discards_readonly_completions() {
900900
let ring = make_ring(8);
901901
let (mut producer, mut consumer, _) = make_test_producer(&ring);
902902

903903
// Submit 3 entries: RO, RW, RO
904-
let tok_ro1 = send_readonly(&mut producer, b"log1");
904+
let _tok_ro1 = send_readonly(&mut producer, b"log1");
905905
let tok_rw = send_readwrite(&mut producer, b"call", 64);
906-
let tok_ro2 = send_readonly(&mut producer, b"log2");
906+
let _tok_ro2 = send_readonly(&mut producer, b"log2");
907907

908908
// Consumer processes all 3
909909
let (_, c1) = consumer.poll(1024).unwrap().unwrap();
@@ -919,24 +919,16 @@ mod tests {
919919
let (_, c3) = consumer.poll(1024).unwrap().unwrap();
920920
consumer.complete(c3).unwrap(); // ack RO
921921

922-
// Reclaim all 3
922+
// Reclaim all 3 - RO completions are discarded, only RW is buffered
923923
let count = producer.reclaim().unwrap();
924924
assert_eq!(count, 3);
925925

926-
// poll() returns them in order
927-
let cqe1 = producer.poll().unwrap().unwrap();
928-
assert_eq!(cqe1.token, tok_ro1);
929-
assert!(cqe1.data.is_empty());
930-
931-
let cqe2 = producer.poll().unwrap().unwrap();
932-
assert_eq!(cqe2.token, tok_rw);
933-
assert_eq!(&cqe2.data[..], b"result");
934-
935-
let cqe3 = producer.poll().unwrap().unwrap();
936-
assert_eq!(cqe3.token, tok_ro2);
937-
assert!(cqe3.data.is_empty());
926+
// poll() returns only the RW completion
927+
let cqe = producer.poll().unwrap().unwrap();
928+
assert_eq!(cqe.token, tok_rw);
929+
assert_eq!(&cqe.data[..], b"result");
938930

939-
// No more
931+
// No more - RO completions were discarded
940932
assert!(producer.poll().unwrap().is_none());
941933
}
942934

@@ -973,11 +965,7 @@ mod tests {
973965
assert_eq!(&cqe2.data[..], b"reply");
974966
}
975967

976-
/// Regression test: reclaim + submit must not cause token collisions.
977-
///
978-
/// Before the monotonic generation counter, Token wrapped the descriptor
979-
/// ID which gets recycled. This caused stale pending completions to
980-
/// match newly submitted entries with the same recycled descriptor ID.
968+
/// reclaim + submit must not cause token collisions.
981969
#[test]
982970
fn test_reclaim_submit_no_token_collision() {
983971
let ring = make_ring(8);
@@ -989,7 +977,6 @@ mod tests {
989977
let (_, c) = consumer.poll(1024).unwrap().unwrap();
990978
consumer.complete(c).unwrap();
991979

992-
// Reclaim pushes the completion to pending (token = tok_old)
993980
let count = producer.reclaim().unwrap();
994981
assert_eq!(count, 1);
995982

@@ -1010,15 +997,42 @@ mod tests {
1010997
wc.write_all(b"result").unwrap();
1011998
consumer.complete(wc.into()).unwrap();
1012999

1013-
// Poll should return the stale ReadOnly completion first (wrong token)
1014-
let cqe1 = producer.poll().unwrap().unwrap();
1015-
assert_eq!(cqe1.token, tok_old);
1016-
assert!(cqe1.data.is_empty());
1000+
// Poll returns only the RW completion (RO was discarded by reclaim)
1001+
let cqe = producer.poll().unwrap().unwrap();
1002+
assert_eq!(cqe.token, tok_new);
1003+
assert_eq!(&cqe.data[..], b"result");
10171004

1018-
// Then the new ReadWrite completion (matching token)
1019-
let cqe2 = producer.poll().unwrap().unwrap();
1020-
assert_eq!(cqe2.token, tok_new);
1021-
assert_eq!(&cqe2.data[..], b"result");
1005+
// No stale RO completion in the queue
1006+
assert!(producer.poll().unwrap().is_none());
1007+
}
1008+
1009+
/// Verify that repeated oneshot submit/reclaim cycles do not accumulate pending completions.
1010+
#[test]
1011+
fn test_reclaim_readonly_does_not_leak_pending() {
1012+
let ring = make_ring(4);
1013+
let (mut producer, mut consumer, _) = make_test_producer(&ring);
1014+
1015+
for _ in 0..10 {
1016+
// Fill the ring
1017+
for _ in 0..4 {
1018+
send_readonly(&mut producer, b"msg");
1019+
}
1020+
1021+
// Consumer acks all
1022+
while let Some((_, completion)) = consumer.poll(1024).unwrap() {
1023+
consumer.complete(completion).unwrap();
1024+
}
1025+
1026+
// Reclaim frees ring slots; empty completions are discarded
1027+
let count = producer.reclaim().unwrap();
1028+
assert_eq!(count, 4);
1029+
1030+
// No completions should be buffered in pending
1031+
assert!(
1032+
producer.poll().unwrap().is_none(),
1033+
"pending should be empty after reclaiming RO entries"
1034+
);
1035+
}
10221036
}
10231037
}
10241038
#[cfg(all(test, loom))]

src/hyperlight_common/src/virtq/producer.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ pub struct RecvCompletion {
3434
pub token: Token,
3535
/// Completion data from the device.
3636
pub data: Bytes,
37+
/// Whether this entry is oneshot so there is no writable completion buffer.
38+
/// Oneshot entries are fire-and-forget: the producer does not
39+
/// expect any response data from the device.
40+
pub oneshot: bool,
3741
}
3842

3943
/// Allocation tracking for an in-flight descriptor chain.
@@ -146,15 +150,16 @@ where
146150
/// * `pool` - Buffer allocator for entry/completion data
147151
pub fn new(layout: Layout, mem: M, notifier: N, pool: P) -> Self {
148152
let inner = RingProducer::new(layout, mem);
149-
let inflight = vec![None; inner.len()];
153+
let ring_len = inner.len();
154+
let inflight = vec![None; ring_len];
150155

151156
Self {
152157
inner,
153158
pool,
154159
notifier,
155160
inflight,
156161
next_token: 0,
157-
pending: VecDeque::new(),
162+
pending: VecDeque::with_capacity(ring_len),
158163
}
159164
}
160165

@@ -192,6 +197,9 @@ where
192197
/// buffer allocations immediately, and buffers completion data for
193198
/// later retrieval via [`poll`](Self::poll).
194199
///
200+
/// Completions with empty data from read-only/oneshot entries are
201+
/// discarded immediately.
202+
///
195203
/// Use this to free resources under backpressure without losing
196204
/// completion data. Returns the number of entries reclaimed.
197205
pub fn reclaim(&mut self) -> Result<usize, VirtqError>
@@ -201,7 +209,11 @@ where
201209
{
202210
let mut count = 0;
203211
while let Some(cqe) = self.poll_ring()? {
204-
self.pending.push_back(cqe);
212+
if !cqe.oneshot {
213+
debug_assert!(self.pending.len() < self.inflight.len());
214+
debug_assert!(!cqe.data.is_empty());
215+
self.pending.push_back(cqe);
216+
}
205217
count += 1;
206218
}
207219
Ok(count)
@@ -242,6 +254,7 @@ where
242254
}
243255

244256
// Read completion data
257+
let has_completion = inf.completion().is_some();
245258
let data = match inf.completion() {
246259
Some(buf) => {
247260
if written > buf.len {
@@ -259,7 +272,11 @@ where
259272
None => Bytes::new(),
260273
};
261274

262-
Ok(Some(RecvCompletion { token, data }))
275+
Ok(Some(RecvCompletion {
276+
token,
277+
data,
278+
oneshot: !has_completion,
279+
}))
263280
}
264281

265282
/// Drain all available completions, calling the provided closure for each.

src/hyperlight_common/src/virtq/ring.rs

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,14 @@ impl RingCursor {
350350
}
351351
}
352352

353-
/// Advance by n positions
353+
/// Advance by n positions using modular arithmetic.
354354
#[inline]
355355
pub(crate) fn advance_by(&mut self, n: u16) {
356-
for _ in 0..n {
357-
self.advance();
356+
let new = self.head + n;
357+
let wraps = new / self.size;
358+
self.head = new % self.size;
359+
if wraps % 2 != 0 {
360+
self.wrap = !self.wrap;
358361
}
359362
}
360363

@@ -371,6 +374,7 @@ impl RingCursor {
371374
}
372375

373376
/// Reset cursor to initial state.
377+
#[inline]
374378
pub fn reset(&mut self) {
375379
self.head = 0;
376380
self.wrap = true;
@@ -962,7 +966,7 @@ impl<M: MemOps> RingConsumer<M> {
962966
return Err(RingError::WouldBlock);
963967
}
964968

965-
// Build chain (head + tails).
969+
// Build chain (head + tails), tracking readable/writable split inline.
966970
let mut elements = SmallVec::<[BufferElement; 16]>::new();
967971
let mut pos = self.avail_cursor;
968972
let mut chain_len: u16 = 1;
@@ -972,7 +976,10 @@ impl<M: MemOps> RingConsumer<M> {
972976

973977
let max_steps = self.desc_table.len();
974978

975-
elements.push(BufferElement::from(&head_desc));
979+
let head_elem = BufferElement::from(&head_desc);
980+
let mut seen_writable = head_elem.writable;
981+
let mut writables: usize = if seen_writable { 1 } else { 0 };
982+
elements.push(head_elem);
976983
pos.advance();
977984

978985
while has_next && steps < max_steps {
@@ -982,8 +989,17 @@ impl<M: MemOps> RingConsumer<M> {
982989
.ok_or(RingError::InvalidState)?;
983990

984991
// tail reads does not need ordering because head has been already validated
985-
let desc = self.mem.read_val(addr).map_err(|_| RingError::MemError)?;
986-
elements.push(BufferElement::from(&desc));
992+
let desc: Descriptor = self.mem.read_val(addr).map_err(|_| RingError::MemError)?;
993+
let elem = BufferElement::from(&desc);
994+
995+
if elem.writable {
996+
seen_writable = true;
997+
writables += 1;
998+
} else if seen_writable {
999+
return Err(RingError::BadChain);
1000+
}
1001+
1002+
elements.push(elem);
9871003

9881004
chain_len += 1;
9891005
steps += 1;
@@ -997,8 +1013,7 @@ impl<M: MemOps> RingConsumer<M> {
9971013
return Err(RingError::BadChain);
9981014
}
9991015

1000-
// Verify that readable/writable split is correct
1001-
let readables = chain_readable_count(&elements)?;
1016+
let readables = elements.len() - writables;
10021017

10031018
// Since driver wrote the same id everywhere, head_desc.id is valid.
10041019
let id = head_desc.id;
@@ -1261,24 +1276,6 @@ pub fn ring_need_event(event_idx: u16, new: u16, old: u16) -> bool {
12611276
new.wrapping_sub(event_idx).wrapping_sub(1) < new.wrapping_sub(old)
12621277
}
12631278

1264-
#[inline]
1265-
/// Check that a buffer chain is well-formed: all readable buffers first,
1266-
/// then writable and return the count of readable buffers.
1267-
fn chain_readable_count(elems: &[BufferElement]) -> Result<usize, RingError> {
1268-
let mut seen_writable = false;
1269-
let mut writables = 0;
1270-
1271-
for e in elems {
1272-
if e.writable {
1273-
seen_writable = true;
1274-
writables += 1;
1275-
} else if seen_writable {
1276-
return Err(RingError::BadChain);
1277-
}
1278-
}
1279-
1280-
Ok(elems.len() - writables)
1281-
}
12821279

12831280
impl From<&Descriptor> for BufferElement {
12841281
fn from(desc: &Descriptor) -> Self {

src/hyperlight_host/benches/benchmarks.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ impl SandboxSize {
6262
Self::Medium => {
6363
let mut cfg = SandboxConfiguration::default();
6464
cfg.set_heap_size(MEDIUM_HEAP_SIZE);
65-
cfg.set_scratch_size(0x50000);
65+
cfg.set_scratch_size(0x80000);
6666
Some(cfg)
6767
}
6868
Self::Large => {
6969
let mut cfg = SandboxConfiguration::default();
7070
cfg.set_heap_size(LARGE_HEAP_SIZE);
71-
cfg.set_scratch_size(0x100000);
71+
cfg.set_scratch_size(0x200000);
7272
Some(cfg)
7373
}
7474
}
@@ -384,10 +384,15 @@ fn guest_call_benchmark_large_param(c: &mut Criterion) {
384384
let large_vec = vec![0u8; SIZE];
385385
let large_string = String::from_utf8(large_vec.clone()).unwrap();
386386

387+
let h2g_pool_pages = (2 * SIZE + (1024 * 1024)) / 4096;
388+
let heap_size = SIZE as u64 * 15;
389+
387390
let mut config = SandboxConfiguration::default();
388-
config.set_h2g_pool_pages((2 * SIZE + (1024 * 1024)) / 4096); // pool pages for the large input
389-
config.set_heap_size(SIZE as u64 * 15);
390-
config.set_scratch_size(6 * SIZE + 4 * (1024 * 1024)); // Big enough for any data copies, etc.
391+
config.set_h2g_pool_pages(h2g_pool_pages);
392+
config.set_h2g_queue_depth(h2g_pool_pages.next_power_of_two());
393+
config.set_heap_size(heap_size);
394+
// Scratch backs all guest physical pages (heap, page tables, pools).
395+
config.set_scratch_size(heap_size as usize + 4 * 1024 * 1024);
391396

392397
let sandbox = UninitializedSandbox::new(
393398
GuestBinary::FilePath(simple_guest_as_string().unwrap()),

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,21 +1090,21 @@ mod tests {
10901090
assert_eq!(res, 0);
10911091
}
10921092

1093-
// Tests to ensure that many (1000) function calls can be made in a call context with a small stack (24K) and heap(20K).
1093+
// Tests to ensure that many (1000) function calls can be made in a call context with a small stack (24K) and heap(32K).
10941094
// This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
10951095
#[test]
10961096
fn test_with_small_stack_and_heap() {
10971097
let mut cfg = SandboxConfiguration::default();
1098-
cfg.set_heap_size(20 * 1024);
1098+
cfg.set_heap_size(32 * 1024);
10991099
// min_scratch_size already includes 1 page (4k on most
11001100
// platforms) of guest stack, so add 20k more to get 24k
11011101
// total, and then add some more for the eagerly-copied page
1102-
// tables on amd64
1102+
// tables on amd64 and virtq pool pages.
11031103
let min_scratch = hyperlight_common::layout::min_scratch_size(
11041104
cfg.get_g2h_queue_depth(),
11051105
cfg.get_h2g_queue_depth(),
11061106
);
1107-
cfg.set_scratch_size(min_scratch + 0x10000 + 0x10000);
1107+
cfg.set_scratch_size(min_scratch + 0x10000 + 0x18000);
11081108

11091109
let mut sbox1: MultiUseSandbox = {
11101110
let path = simple_guest_as_string().unwrap();
@@ -1718,7 +1718,7 @@ mod tests {
17181718

17191719
for (name, heap_size) in test_cases {
17201720
let mut cfg = SandboxConfiguration::default();
1721-
cfg.set_heap_size(heap_size);
1721+
cfg.set_heap_size(128 * 1024);
17221722
cfg.set_scratch_size(0x100000);
17231723

17241724
let path = simple_guest_as_string().unwrap();

0 commit comments

Comments
 (0)