Skip to content

Commit 447dc13

Browse files
authored
feat(virtq): add high-level packed virtqueue producer/consumer api (#1514)
* feat(virtq): add high-level packed virtqueue producer/consumer api This patch adds a higher level api on top of the packed ring primitives in hyperlight_common so callers don't manage raw descriptors directly. - Add VirtqProducer/VirtqConsumer in producer.rs and consumer.rs that handle buffer allocation, chain lifecycle, and event suppression/notification decisions; expand virtq/mod.rs with the public API, builders, and docs. - Add buffer management: the BufferProvider trait and shared types in buffer.rs plus two concrete allocators - a two-tier variable-size BufferPool and a fixed-slot RecyclePool. - Add an 8-byte wire message header in msg.rs for message type discrimination and request/response correlation. - Add loom-based concurrency tests in concurrency.rs using shadow atomics - Add criterion benchmarks for the buffer pool and virtq API. Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix: replace manual implementations of is_multiple_of Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): address code review Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> --------- Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent 96e51bb commit 447dc13

19 files changed

Lines changed: 6851 additions & 54 deletions

File tree

.github/workflows/dep_code_checks.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ jobs:
9090
just test-compilation-no-default-features debug
9191
just test-compilation-no-default-features release
9292
93+
- name: Run loom concurrency tests
94+
run: just test-loom
95+
9396
windows-checks:
9497
if: ${{ inputs.docs_only == 'false' }}
9598
timeout-minutes: 30

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ hyperlight-component-macro = { path = "src/hyperlight_component_macro", version
4949

5050
[workspace.lints.rust]
5151
unsafe_op_in_unsafe_fn = "deny"
52+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(loom)'] }
5253

5354
# this will generate symbols for release builds
5455
# so is handy for debugging issues in release builds

Justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ test target=default-target features="": (test-unit target features) (test-isolat
229229
test-unit target=default-target features="":
230230
{{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib
231231

232+
# runs loom concurrency tests
233+
test-loom:
234+
{{ set-env-command }}RUSTFLAGS='--cfg loom'; {{ cargo-cmd }} test --quiet -p hyperlight-common --lib {{ target-triple-flag }} virtq::concurrency::
235+
232236
# runs tests that requires being run separately, for example due to global state
233237
test-isolated target=default-target features="" :
234238
{{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- sandbox::uninitialized::tests::test_log_trace --exact --ignored

src/hyperlight_common/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ arbitrary = {version = "1.4.2", optional = true, features = ["derive"]}
1919
anyhow = { version = "1.0.102", default-features = false }
2020
bitflags = "2.13.0"
2121
bytemuck = { version = "1.24", features = ["derive"] }
22+
bytes = { version = "1", default-features = false }
23+
fixedbitset = { version = "0.5.7", default-features = false }
2224
flatbuffers = { version = "25.12.19", default-features = false }
2325
log = "0.4.32"
2426
smallvec = "1.15.1"
@@ -36,9 +38,22 @@ mem_profile = []
3638
std = ["thiserror/std", "log/std", "tracing/std"]
3739

3840
[dev-dependencies]
41+
criterion = "0.8.1"
42+
hyperlight-testing = { workspace = true }
3943
quickcheck = "1.0.3"
4044
rand = "0.10.1"
4145

46+
[target.'cfg(loom)'.dev-dependencies]
47+
loom = "0.7"
48+
4249
[lib]
4350
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
4451
doctest = false # reduce noise in test output
52+
53+
[[bench]]
54+
name = "buffer_pool"
55+
harness = false
56+
57+
[[bench]]
58+
name = "virtq_api"
59+
harness = false
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
Copyright 2026 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
use std::hint::black_box;
18+
19+
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
20+
use hyperlight_common::virtq::{BufferPool, BufferProvider, RecyclePool};
21+
22+
// Helper to create a pool for benchmarking
23+
fn make_pool<const L: usize, const U: usize>(size: usize) -> BufferPool<L, U> {
24+
let base = 0x10000;
25+
BufferPool::<L, U>::new(base, size).unwrap()
26+
}
27+
28+
// Single allocation performance
29+
fn bench_alloc_single(c: &mut Criterion) {
30+
let mut group = c.benchmark_group("alloc_single");
31+
32+
for size in [64, 128, 256, 512, 1024, 1500, 4096].iter() {
33+
group.throughput(Throughput::Elements(1));
34+
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
35+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
36+
b.iter(|| {
37+
let alloc = pool.alloc(black_box(size)).unwrap();
38+
pool.dealloc(alloc.addr).unwrap();
39+
});
40+
});
41+
}
42+
group.finish();
43+
}
44+
45+
// LIFO recycling
46+
fn bench_alloc_lifo(c: &mut Criterion) {
47+
let mut group = c.benchmark_group("alloc_lifo");
48+
49+
for size in [256, 1500, 4096].iter() {
50+
group.throughput(Throughput::Elements(100));
51+
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
52+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
53+
b.iter(|| {
54+
for _ in 0..100 {
55+
let alloc = pool.alloc(black_box(size)).unwrap();
56+
pool.dealloc(alloc.addr).unwrap();
57+
}
58+
});
59+
});
60+
}
61+
group.finish();
62+
}
63+
64+
// Fragmented allocation worst case
65+
fn bench_alloc_fragmented(c: &mut Criterion) {
66+
let mut group = c.benchmark_group("alloc_fragmented");
67+
68+
group.bench_function("fragmented_256", |b| {
69+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
70+
71+
// Create fragmentation pattern: allocate many, free every other
72+
let mut allocations = Vec::new();
73+
for _ in 0..100 {
74+
allocations.push(pool.alloc(128).unwrap());
75+
}
76+
for i in (0..100).step_by(2) {
77+
pool.dealloc(allocations[i].addr).unwrap();
78+
}
79+
80+
b.iter(|| {
81+
let alloc = pool.alloc(black_box(256)).unwrap();
82+
pool.dealloc(alloc.addr).unwrap();
83+
});
84+
});
85+
86+
group.finish();
87+
}
88+
89+
// Free performance
90+
fn bench_free(c: &mut Criterion) {
91+
let mut group = c.benchmark_group("free");
92+
93+
for size in [256, 1500, 4096].iter() {
94+
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
95+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
96+
b.iter(|| {
97+
let alloc = pool.alloc(size).unwrap();
98+
pool.dealloc(black_box(alloc.addr)).unwrap();
99+
});
100+
});
101+
}
102+
103+
group.finish();
104+
}
105+
106+
// Free-list reuse
107+
fn bench_free_list_reuse(c: &mut Criterion) {
108+
let mut group = c.benchmark_group("free_list_reuse");
109+
110+
// With cursor optimization (LIFO)
111+
group.bench_function("lifo_pattern", |b| {
112+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
113+
b.iter(|| {
114+
let alloc = pool.alloc(256).unwrap();
115+
pool.dealloc(alloc.addr).unwrap();
116+
let alloc2 = pool.alloc(black_box(256)).unwrap();
117+
pool.dealloc(alloc2.addr).unwrap();
118+
});
119+
});
120+
121+
// Without cursor benefit (FIFO-like)
122+
group.bench_function("fifo_pattern", |b| {
123+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
124+
let mut queue = Vec::new();
125+
126+
// Pre-fill queue
127+
for _ in 0..10 {
128+
queue.push(pool.alloc(256).unwrap());
129+
}
130+
131+
b.iter(|| {
132+
// FIFO: free oldest, allocate new
133+
let old = queue.remove(0);
134+
pool.dealloc(old.addr).unwrap();
135+
queue.push(pool.alloc(black_box(256)).unwrap());
136+
});
137+
});
138+
139+
group.finish();
140+
}
141+
142+
// Segmented logical payload allocation
143+
fn bench_segmented_payload(c: &mut Criterion) {
144+
let mut group = c.benchmark_group("segmented_payload");
145+
146+
for payload_size in [8 * 1024usize, 64 * 1024, 256 * 1024] {
147+
group.throughput(Throughput::Bytes(payload_size as u64));
148+
group.bench_with_input(
149+
BenchmarkId::from_parameter(payload_size),
150+
&payload_size,
151+
|b, &payload_size| {
152+
let pool = make_pool::<256, 4096>(4 * 1024 * 1024);
153+
b.iter(|| {
154+
let sgs = pool.alloc_sg(black_box(payload_size)).unwrap();
155+
for sg in sgs {
156+
pool.dealloc(sg.addr).unwrap();
157+
}
158+
});
159+
},
160+
);
161+
}
162+
163+
group.finish();
164+
}
165+
166+
fn bench_recycle_pool(c: &mut Criterion) {
167+
let mut group = c.benchmark_group("recycle_pool");
168+
169+
group.bench_function("alloc_dealloc_4096", |b| {
170+
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 4096).unwrap();
171+
b.iter(|| {
172+
let alloc = pool.alloc(black_box(4096)).unwrap();
173+
pool.dealloc(alloc.addr).unwrap();
174+
});
175+
});
176+
177+
group.bench_function("alloc_dealloc_128", |b| {
178+
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 256).unwrap();
179+
b.iter(|| {
180+
let alloc = pool.alloc(black_box(128)).unwrap();
181+
pool.dealloc(alloc.addr).unwrap();
182+
});
183+
});
184+
185+
group.bench_function("alloc_dealloc_1500", |b| {
186+
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 4096).unwrap();
187+
b.iter(|| {
188+
let alloc = pool.alloc(black_box(1500)).unwrap();
189+
pool.dealloc(alloc.addr).unwrap();
190+
});
191+
});
192+
193+
group.bench_function("alloc_sg_64k", |b| {
194+
let pool = RecyclePool::new(0x80000, 4 * 1024 * 1024, 4096).unwrap();
195+
b.iter(|| {
196+
let sgs = pool.alloc_sg(black_box(64 * 1024)).unwrap();
197+
for sg in sgs {
198+
pool.dealloc(sg.addr).unwrap();
199+
}
200+
});
201+
});
202+
203+
group.finish();
204+
}
205+
206+
criterion_group!(
207+
benches,
208+
bench_alloc_single,
209+
bench_alloc_lifo,
210+
bench_alloc_fragmented,
211+
bench_free,
212+
bench_free_list_reuse,
213+
bench_segmented_payload,
214+
bench_recycle_pool,
215+
);
216+
217+
criterion_main!(benches);

0 commit comments

Comments
 (0)