···11+(* This benchmark uses [Uring.readv] to read from /dev/zero in a loop.
22+ This is intended to stress the sketch buffer. *)
33+44+let buffer_size = 100 (* Use a small buffer to stress the system more *)
55+let n_concurrent = 16 (* How many requests to have active at once *)
66+let n_iters = 1_000_000 (* How many times to accept and resubmit *)
77+88+let rec wait t handle =
99+ match Uring.peek t with
1010+ | Some { result; data = buf } -> handle result buf
1111+ | None ->
1212+ match Uring.wait t with
1313+ | None -> wait t handle
1414+ | Some { result; data = buf } -> handle result buf
1515+1616+let run_bechmark ~polling_timeout fd =
1717+ let got = ref 0 in
1818+ (* For polling mode, [queue_depth] needs to be slightly larger than [n_concurrent] or submission
1919+ occasionally fails for some reason. *)
2020+ let t = Uring.create ?polling_timeout ~queue_depth:(n_concurrent * 2) () in
2121+ (* We start by submitting [n_concurrent] reads. *)
2222+ for _ = 1 to n_concurrent do
2323+ let buf = Cstruct.create buffer_size in
2424+ let _job : _ Uring.job = Uring.readv t fd [buf] ~file_offset:Optint.Int63.zero [buf] |> Option.get in
2525+ ()
2626+ done;
2727+ assert (Uring.submit t = n_concurrent);
2828+ (* Then we wait for reads to complete. Each time a read returns, we immediately submit another. *)
2929+ let t0 = Unix.gettimeofday () in
3030+ for _ = 1 to n_iters do
3131+ wait t (fun result bufs ->
3232+ if result < 0 then (
3333+ raise (Unix.Unix_error (Uring.error_of_errno result, "readv", ""))
3434+ ) else (
3535+ got := !got + result;
3636+ let _job : _ Uring.job = Uring.readv t fd bufs ~file_offset:Optint.Int63.zero bufs |> Option.get in
3737+ ()
3838+ )
3939+ )
4040+ done;
4141+ (* Get a snapshot of the stats before letting things finish. *)
4242+ let stats = Uring.get_debug_stats t in
4343+ let t1 = Unix.gettimeofday () in
4444+ let time = t1 -. t0 in
4545+ let got = float !got /. (1024. *. 1024.) in
4646+ let polling = polling_timeout <> None in
4747+ Fmt.pr "@[<v2>Read %.2f MB in %.2f seconds (%.2f MB/second) # buffer_size=%d, polling=%b@,%a@]@."
4848+ got time (got /. time) buffer_size polling
4949+ Uring.Stats.pp stats;
5050+ (* Finally, drain the remaining reads and shut down the ring. *)
5151+ for _ = 1 to n_concurrent do
5252+ wait t (fun _result _buf -> ())
5353+ done;
5454+ Uring.exit t
5555+5656+let () =
5757+ let fd = Unix.openfile "/dev/zero" Unix.[O_RDONLY] 0 in
5858+ run_bechmark fd ~polling_timeout:None;
5959+ run_bechmark fd ~polling_timeout:(Some 1000);
6060+ run_bechmark fd ~polling_timeout:None;
6161+ run_bechmark fd ~polling_timeout:(Some 1000);
6262+ Unix.close fd
+57-10
lib/uring/uring.ml
···133133134134 let create_buffer len = Bigarray.(Array1.create char c_layout len)
135135136136- let create len =
137137- { buffer = create_buffer len; off = 0; old_buffers = [] }
136136+ let create () =
137137+ { buffer = Cstruct.empty.buffer; off = 0; old_buffers = [] }
138138139139 let length t = Bigarray.Array1.size_in_bytes t.buffer
140140···146146 let alloc t alloc_len =
147147 let alloc_len = round alloc_len in
148148 if alloc_len > avail t then begin
149149- let new_buffer = create_buffer ((length t) + alloc_len) in
149149+ (* At least 64 bytes, at least twice the previous size, and
150150+ at least big enough for the new allocation. *)
151151+ let new_size = max 64 (max (2 * length t) alloc_len) in
152152+ let new_buffer = create_buffer new_size in
150153 t.old_buffers <- t.buffer :: t.old_buffers;
151154 t.off <- 0;
152155 t.buffer <- new_buffer;
···288291 let data = Heap.create queue_depth in
289292 let id = object end in
290293 let fixed_iobuf = Cstruct.empty.buffer in
291291- let sketch = Sketch.create 0 in
294294+ let sketch = Sketch.create () in
292295 let t = { id; uring; sketch; fixed_iobuf; data; dirty=false; queue_depth } in
293296 register_gc_root t;
294297 t
···413416 ignore (Heap.ptr job : Uring.id); (* Check it's still valid *)
414417 with_id t (fun id -> Uring.submit_cancel t.uring id (Heap.ptr job)) user_data
415418419419+(* Free stale entries in the sketch buffer, if possible.
420420+ This isn't quite right: a busy system might never have 0 unsubmitted entries.
421421+ We should probably track how many requests need to be submitted before each
422422+ of [t.sketch.old_buffers] can be released, but this will do for now. *)
423423+let gc_sketch t =
424424+ if Uring.sq_ready t.uring = 0 then Sketch.release t.sketch
425425+416426let submit t =
417427 let v =
418428 if t.dirty then begin
···421431 end else
422432 0
423433 in
424424- if Uring.sq_ready t.uring = 0 then
425425- Sketch.release t.sketch;
434434+ (* In non-polling mode, we will almost always be able to free the sketch buffer here.
435435+ However, in polling mode it's unlikely the entries have been consumed by the kernel yet,
436436+ and we must rely on other GC points. *)
437437+ gc_sketch t;
426438 v
427439428440type 'a completion_option =
···436448 let data = Heap.free t.data user_data_id in
437449 Some { result = res; data }
438450439439-let peek t = fn_on_ring Uring.peek_cqe t
451451+let peek t =
452452+ gc_sketch t;
453453+ fn_on_ring Uring.peek_cqe t
440454441455let wait ?timeout t =
442442- match timeout with
443443- | None -> fn_on_ring Uring.wait_cqe t
444444- | Some timeout -> fn_on_ring (Uring.wait_cqe_timeout timeout) t
456456+ let r =
457457+ match timeout with
458458+ | None -> fn_on_ring Uring.wait_cqe t
459459+ | Some timeout -> fn_on_ring (Uring.wait_cqe_timeout timeout) t
460460+ in
461461+ (* In polling mode, this is a good time to GC the sketch buffer, because the
462462+ kernel has probably consumed all the enties while we were blocking. *)
463463+ gc_sketch t;
464464+ r
445465446466let queue_depth {queue_depth;_} = queue_depth
447467let buf {fixed_iobuf;_} = fixed_iobuf
448468449469let error_of_errno e =
450470 Uring.error_of_errno (abs e)
471471+472472+module Stats = struct
473473+ type t = {
474474+ sqe_ready : int;
475475+ active_ops : int;
476476+ sketch_buffer_size : int;
477477+ sketch_used : int;
478478+ sketch_old_buffers : int;
479479+ }
480480+481481+ let pp f { sqe_ready; active_ops; sketch_used; sketch_buffer_size; sketch_old_buffers } =
482482+ Fmt.pf f "@[<v>SQEs ready: %d@,\
483483+ Operations active: %d@,\
484484+ Sketch buffer: %d/%d (plus %d old buffers)@]"
485485+ sqe_ready
486486+ active_ops
487487+ sketch_used sketch_buffer_size sketch_old_buffers
488488+end
489489+490490+let get_debug_stats t =
491491+ { Stats.
492492+ sqe_ready = Uring.sq_ready t.uring;
493493+ active_ops = Heap.in_use t.data;
494494+ sketch_used = t.sketch.off;
495495+ sketch_buffer_size = Bigarray.Array1.dim t.sketch.buffer;
496496+ sketch_old_buffers = List.length t.sketch.old_buffers;
497497+ }
+15
lib/uring/uring.mli
···271271val error_of_errno : int -> Unix.error
272272(** [error_of_errno e] converts the error code [abs e] to a Unix error type. *)
273273274274+module Stats : sig
275275+ type t = {
276276+ sqe_ready : int;
277277+ active_ops : int;
278278+ sketch_buffer_size : int;
279279+ sketch_used : int;
280280+ sketch_old_buffers : int;
281281+ }
282282+283283+ val pp : t Fmt.t
284284+end
285285+286286+val get_debug_stats : _ t -> Stats.t
287287+(** [get_debug_stats t] collects some metrics about the internal state of [t]. *)
288288+274289module Private : sig
275290 module Heap = Heap
276291end