revert: remove name on futures and tasks

async tracing will be more robust, and is enabled by
task local storage
This commit is contained in:
Simon Cruanes 2024-02-14 21:05:35 -05:00
parent b0d2716eff
commit cf8555bcec
No known key found for this signature in database
GPG key ID: EBFFF6F283F3A2B4
16 changed files with 80 additions and 138 deletions

View file

@ -14,7 +14,7 @@ let cutoff = ref 20
let rec fib ~on x : int Fut.t =
if x <= !cutoff then
Fut.spawn ~name:"fib" ~on (fun () -> fib_direct x)
Fut.spawn ~on (fun () -> fib_direct x)
else
let open Fut.Infix in
let+ t1 = fib ~on (x - 1) and+ t2 = fib ~on (x - 2) in
@ -31,14 +31,14 @@ let fib_fj ~on x : int Fut.t =
n1 + n2
)
in
Fut.spawn ~name:"fib" ~on (fun () -> fib_rec x)
Fut.spawn ~on (fun () -> fib_rec x)
let fib_await ~on x : int Fut.t =
let rec fib_rec x : int Fut.t =
if x <= !cutoff then
Fut.spawn ~name:"fib" ~on (fun () -> fib_direct x)
Fut.spawn ~on (fun () -> fib_direct x)
else
Fut.spawn ~name:"fib" ~on (fun () ->
Fut.spawn ~on (fun () ->
let n1 = fib_rec (x - 1) in
let n2 = fib_rec (x - 2) in
let n1 = Fut.await n1 in

View file

@ -76,7 +76,7 @@ let run_fork_join ~kind num_steps : float =
let step = 1. /. float num_steps in
let global_sum = Lock.create 0. in
Ws_pool.run_wait_block ~name:"pi.fj" pool (fun () ->
Ws_pool.run_wait_block pool (fun () ->
FJ.for_
~chunk_size:(3 + (num_steps / num_tasks))
num_steps

View file

@ -6,7 +6,6 @@ let k_storage = Task_local_storage.Private_.Storage.k_storage
type task_full = {
f: unit -> unit;
name: string;
ls: Task_local_storage.storage;
}
@ -44,18 +43,17 @@ let worker_thread_ (self : state) (runner : t) ~on_exn ~around_task : unit =
!cur_ls
in
let run_another_task ls ~name task' =
let run_another_task ls task' =
let ls' = Task_local_storage.Private_.Storage.copy ls in
schedule_ self { f = task'; name; ls = ls' }
schedule_ self { f = task'; ls = ls' }
in
let run_task (task : task_full) : unit =
cur_ls := task.ls;
let _ctx = before_task runner in
cur_span := Tracing_.enter_span task.name;
let resume ls k res =
schedule_ self { f = (fun () -> k res); name = task.name; ls }
schedule_ self { f = (fun () -> k res); ls }
in
(* run the task now, catching errors, handling effects *)
@ -131,7 +129,7 @@ let create ?(on_init_thread = default_thread_init_exit_)
{ threads = Array.make num_threads dummy; q = Bb_queue.create () }
in
let run_async ~name ~ls f = schedule_ pool { f; name; ls } in
let run_async ~ls f = schedule_ pool { f; ls } in
let runner =
Runner.For_runner_implementors.create

View file

@ -5,21 +5,13 @@ type 'a waiter = 'a or_error -> unit
type 'a state =
| Done of 'a or_error
| Waiting of {
waiters: 'a waiter list;
name: string;
}
| Waiting of { waiters: 'a waiter list }
type 'a t = { st: 'a state A.t } [@@unboxed]
type 'a promise = 'a t
let[@inline] get_name_ (self : _ t) =
match A.get self.st with
| Done _ -> ""
| Waiting { name; _ } -> name
let make ?(name = "") () =
let fut = { st = A.make (Waiting { waiters = []; name }) } in
let make () =
let fut = { st = A.make (Waiting { waiters = [] }) } in
fut, fut
let[@inline] of_result x : _ t = { st = A.make (Done x) }
@ -72,8 +64,8 @@ let on_result (self : _ t) (f : _ waiter) : unit =
| Done x ->
f x;
false
| Waiting { waiters = l; name } ->
not (A.compare_and_set self.st st (Waiting { waiters = f :: l; name }))
| Waiting { waiters = l } ->
not (A.compare_and_set self.st st (Waiting { waiters = f :: l }))
do
Domain_.relax ()
done
@ -86,7 +78,7 @@ let fulfill (self : _ t) (r : _ result) : unit =
let st = A.get self.st in
match st with
| Done _ -> raise Already_fulfilled
| Waiting { waiters = l; name = _ } ->
| Waiting { waiters = l } ->
let did_swap = A.compare_and_set self.st st (Done r) in
if did_swap then (
(* success, now call all the waiters *)
@ -105,7 +97,7 @@ let[@inline] fulfill_idempotent self r =
(* ### combinators ### *)
let spawn ?name ?ls ~on f : _ t =
let spawn ?ls ~on f : _ t =
let fut, promise = make () in
let task () =
@ -118,13 +110,13 @@ let spawn ?name ?ls ~on f : _ t =
fulfill promise res
in
Runner.run_async ?name ?ls on task;
Runner.run_async ?ls on task;
fut
let spawn_on_current_runner ?name ?ls f : _ t =
let spawn_on_current_runner ?ls f : _ t =
match Runner.get_current_runner () with
| None -> failwith "Fut.spawn_on_current_runner: not running on a runner"
| Some on -> spawn ?name ?ls ~on f
| Some on -> spawn ?ls ~on f
let reify_error (f : 'a t) : 'a or_error t =
match peek f with
@ -150,22 +142,20 @@ let map ?on ~f fut : _ t =
| Error e_bt -> Error e_bt
in
let name = get_name_ fut in
match peek fut, get_runner_ ?on () with
| Some res, None -> of_result @@ map_immediate_ res
| Some res, Some runner ->
let fut2, promise = make ~name () in
Runner.run_async ~name runner (fun () ->
fulfill promise @@ map_immediate_ res);
let fut2, promise = make () in
Runner.run_async runner (fun () -> fulfill promise @@ map_immediate_ res);
fut2
| None, None ->
let fut2, promise = make ~name () in
let fut2, promise = make () in
on_result fut (fun res -> fulfill promise @@ map_immediate_ res);
fut2
| None, Some runner ->
let fut2, promise = make ~name () in
let fut2, promise = make () in
on_result fut (fun res ->
Runner.run_async ~name runner (fun () ->
Runner.run_async runner (fun () ->
fulfill promise @@ map_immediate_ res));
fut2
@ -174,7 +164,7 @@ let join (fut : 'a t t) : 'a t =
| Some (Ok f) -> f
| Some (Error (e, bt)) -> fail e bt
| None ->
let fut2, promise = make ~name:(get_name_ fut) () in
let fut2, promise = make () in
on_result fut (function
| Ok sub_fut -> on_result sub_fut (fulfill promise)
| Error _ as e -> fulfill promise e);
@ -197,20 +187,19 @@ let bind ?on ~f fut : _ t =
on_result f_res_fut (fun r -> fulfill promise r)
in
let name = get_name_ fut in
match peek fut, get_runner_ ?on () with
| Some res, Some runner ->
let fut2, promise = make ~name () in
Runner.run_async ~name runner (bind_and_fulfill res promise);
let fut2, promise = make () in
Runner.run_async runner (bind_and_fulfill res promise);
fut2
| Some res, None -> apply_f_to_res res
| None, Some runner ->
let fut2, promise = make ~name () in
let fut2, promise = make () in
on_result fut (fun r ->
Runner.run_async ~name runner (bind_and_fulfill r promise));
Runner.run_async runner (bind_and_fulfill r promise));
fut2
| None, None ->
let fut2, promise = make ~name () in
let fut2, promise = make () in
on_result fut (fun res -> bind_and_fulfill res promise ());
fut2
@ -234,7 +223,7 @@ let both a b : _ t =
| Some (Ok x), Some (Ok y) -> return (x, y)
| Some (Error (e, bt)), _ | _, Some (Error (e, bt)) -> fail e bt
| _ ->
let fut, promise = make ~name:(get_name_ a) () in
let fut, promise = make () in
let st = A.make `Neither in
on_result a (function
@ -267,7 +256,7 @@ let choose a b : _ t =
| _, Some (Ok y) -> return (Either.Right y)
| Some (Error (e, bt)), Some (Error _) -> fail e bt
| _ ->
let fut, promise = make ~name:(get_name_ a) () in
let fut, promise = make () in
let one_failure = A.make false in
on_result a (function
@ -290,7 +279,7 @@ let choose_same a b : _ t =
| _, Some (Ok y) -> return y
| Some (Error (e, bt)), Some (Error _) -> fail e bt
| _ ->
let fut, promise = make ~name:(get_name_ a) () in
let fut, promise = make () in
let one_failure = A.make false in
on_result a (function

View file

@ -26,9 +26,8 @@ type 'a promise
(** A promise, which can be fulfilled exactly once to set
the corresponding future *)
val make : ?name:string -> unit -> 'a t * 'a promise
(** Make a new future with the associated promise.
@param name name for the future, used for tracing. since NEXT_RELEASE. *)
val make : unit -> 'a t * 'a promise
(** Make a new future with the associated promise. *)
val on_result : 'a t -> ('a or_error -> unit) -> unit
(** [on_result fut f] registers [f] to be called in the future
@ -95,16 +94,12 @@ val is_failed : _ t -> bool
(** {2 Combinators} *)
val spawn :
?name:string ->
?ls:Task_local_storage.storage ->
on:Runner.t ->
(unit -> 'a) ->
'a t
?ls:Task_local_storage.storage -> on:Runner.t -> (unit -> 'a) -> 'a t
(** [spaw ~on f] runs [f()] on the given runner [on], and return a future that will
hold its result. *)
val spawn_on_current_runner :
?name:string -> ?ls:Task_local_storage.storage -> (unit -> 'a) -> 'a t
?ls:Task_local_storage.storage -> (unit -> 'a) -> 'a t
(** This must be run from inside a runner, and schedules
the new task on it as well.

View file

@ -4,19 +4,16 @@ include Runner
(* convenient alias *)
let k_ls = Task_local_storage.Private_.Storage.k_storage
let run_async_ ~name ~ls f =
let run_async_ ~ls f =
let cur_ls = ref ls in
TLS.set k_ls (Some cur_ls);
cur_ls := ls;
let sp = Tracing_.enter_span name in
try
let x = f () in
Tracing_.exit_span sp;
TLS.set k_ls None;
x
with e ->
let bt = Printexc.get_raw_backtrace () in
Tracing_.exit_span sp;
TLS.set k_ls None;
Printexc.raise_with_backtrace e bt

View file

@ -26,25 +26,14 @@ val start_thread_on_some_domain : ('a -> unit) -> 'a -> Thread.t
to run all the various threads needed in an application (timers, event loops, etc.) *)
val run_async :
?name:string ->
?ls:Task_local_storage.storage ->
Runner.t ->
(unit -> unit) ->
unit
?ls:Task_local_storage.storage -> Runner.t -> (unit -> unit) -> unit
(** [run_async runner task] schedules the task to run
on the given runner. This means [task()] will be executed
at some point in the future, possibly in another thread.
@param name if provided and [Trace] is present in dependencies, a span
will be created when the task starts, and will stop when the task is over.
(since NEXT_RELEASE)
@since 0.5 *)
val run_wait_block :
?name:string ->
?ls:Task_local_storage.storage ->
Runner.t ->
(unit -> 'a) ->
'a
?ls:Task_local_storage.storage -> Runner.t -> (unit -> 'a) -> 'a
(** [run_wait_block runner f] schedules [f] for later execution
on the runner, like {!run_async}.
It then blocks the current thread until [f()] is done executing,
@ -63,21 +52,14 @@ val recommended_thread_count : unit -> int
@since 0.5 *)
val spawn :
?name:string ->
?ls:Task_local_storage.storage ->
on:Runner.t ->
(unit -> 'a) ->
'a Fut.t
?ls:Task_local_storage.storage -> on:Runner.t -> (unit -> 'a) -> 'a Fut.t
(** [spawn ~on f] runs [f()] on the runner (a thread pool typically)
and returns a future result for it. See {!Fut.spawn}.
@param name if provided and [Trace] is present in dependencies,
a span will be created for the future. (since 0.6)
@since 0.5 *)
val spawn_on_current_runner :
?name:string -> ?ls:Task_local_storage.storage -> (unit -> 'a) -> 'a Fut.t
?ls:Task_local_storage.storage -> (unit -> 'a) -> 'a Fut.t
(** See {!Fut.spawn_on_current_runner}.
@param name see {!spawn}. since 0.6.
@since 0.5 *)
[@@@ifge 5.0]

View file

@ -3,7 +3,7 @@ module TLS = Thread_local_storage_
type task = unit -> unit
type t = {
run_async: name:string -> ls:Task_local_storage.storage -> task -> unit;
run_async: ls:Task_local_storage.storage -> task -> unit;
shutdown: wait:bool -> unit -> unit;
size: unit -> int;
num_tasks: unit -> int;
@ -11,9 +11,9 @@ type t = {
exception Shutdown
let[@inline] run_async ?(name = "")
?(ls = Task_local_storage.Private_.Storage.create ()) (self : t) f : unit =
self.run_async ~name ~ls f
let[@inline] run_async ?(ls = Task_local_storage.Private_.Storage.create ())
(self : t) f : unit =
self.run_async ~ls f
let[@inline] shutdown (self : t) : unit = self.shutdown ~wait:true ()
@ -23,9 +23,9 @@ let[@inline] shutdown_without_waiting (self : t) : unit =
let[@inline] num_tasks (self : t) : int = self.num_tasks ()
let[@inline] size (self : t) : int = self.size ()
let run_wait_block ?name ?ls self (f : unit -> 'a) : 'a =
let run_wait_block ?ls self (f : unit -> 'a) : 'a =
let q = Bb_queue.create () in
run_async ?name ?ls self (fun () ->
run_async ?ls self (fun () ->
try
let x = f () in
Bb_queue.push q (Ok x)

View file

@ -33,19 +33,14 @@ val shutdown_without_waiting : t -> unit
exception Shutdown
val run_async :
?name:string -> ?ls:Task_local_storage.storage -> t -> task -> unit
val run_async : ?ls:Task_local_storage.storage -> t -> task -> unit
(** [run_async pool f] schedules [f] for later execution on the runner
in one of the threads. [f()] will run on one of the runner's
worker threads/domains.
@param name if provided and [Trace] is present in dependencies, a span
will be created when the task starts, and will stop when the task is over.
(since NEXT_RELEASE)
@param ls if provided, run the task with this initial local storage
@raise Shutdown if the runner was shut down before [run_async] was called. *)
val run_wait_block :
?name:string -> ?ls:Task_local_storage.storage -> t -> (unit -> 'a) -> 'a
val run_wait_block : ?ls:Task_local_storage.storage -> t -> (unit -> 'a) -> 'a
(** [run_wait_block pool f] schedules [f] for later execution
on the pool, like {!run_async}.
It then blocks the current thread until [f()] is done executing,
@ -65,7 +60,7 @@ module For_runner_implementors : sig
size:(unit -> int) ->
num_tasks:(unit -> int) ->
shutdown:(wait:bool -> unit -> unit) ->
run_async:(name:string -> ls:Task_local_storage.storage -> task -> unit) ->
run_async:(ls:Task_local_storage.storage -> task -> unit) ->
unit ->
t
(** Create a new runner.

View file

@ -7,7 +7,7 @@ type task = unit -> unit
type suspension_handler = {
handle:
run:(name:string -> task -> unit) ->
run:(task -> unit) ->
resume:(suspension -> unit Exn_bt.result -> unit) ->
suspension ->
unit;
@ -27,8 +27,7 @@ type with_suspend_handler =
| WSH : {
on_suspend: unit -> 'state;
(** on_suspend called when [f()] suspends itself. *)
run: 'state -> name:string -> task -> unit;
(** run used to schedule new tasks *)
run: 'state -> task -> unit; (** run used to schedule new tasks *)
resume: 'state -> suspension -> unit Exn_bt.result -> unit;
(** resume run the suspension. Must be called exactly once. *)
}

View file

@ -14,7 +14,7 @@ type task = unit -> unit
type suspension_handler = {
handle:
run:(name:string -> task -> unit) ->
run:(task -> unit) ->
resume:(suspension -> unit Exn_bt.result -> unit) ->
suspension ->
unit;
@ -24,7 +24,6 @@ type suspension_handler = {
The handler is given a few things:
- the name (if any) of the current computation
- the suspended computation (which can be resumed with a result
eventually);
- a [run] function that can be used to start tasks to perform some
@ -70,8 +69,7 @@ type with_suspend_handler =
| WSH : {
on_suspend: unit -> 'state;
(** on_suspend called when [f()] suspends itself. *)
run: 'state -> name:string -> task -> unit;
(** run used to schedule new tasks *)
run: 'state -> task -> unit; (** run used to schedule new tasks *)
resume: 'state -> suspension -> unit Exn_bt.result -> unit;
(** resume run the suspension. Must be called exactly once. *)
}

View file

@ -16,7 +16,6 @@ end
type task_full = {
f: task;
name: string;
ls: Task_local_storage.storage;
}
@ -26,7 +25,6 @@ type worker_state = {
pool_id_: Id.t; (** Unique per pool *)
mutable thread: Thread.t;
q: task_full WSQ.t; (** Work stealing queue *)
mutable cur_span: int64;
cur_ls: Task_local_storage.storage ref; (** Task storage *)
rng: Random.State.t;
}
@ -75,10 +73,10 @@ let[@inline] try_wake_someone_ (self : state) : unit =
)
(** Run [task] as is, on the pool. *)
let schedule_task_ (self : state) ~name ~ls (w : worker_state option) (f : task)
let schedule_task_ (self : state) ~ls (w : worker_state option) (f : task)
: unit =
(* Printf.printf "schedule task now (%d)\n%!" (Thread.id @@ Thread.self ()); *)
let task = { f; name; ls } in
let task = { f; ls } in
match w with
| Some w when Id.equal self.id_ w.pool_id_ ->
(* we're on this same pool, schedule in the worker's state. Otherwise
@ -107,33 +105,26 @@ let schedule_task_ (self : state) ~name ~ls (w : worker_state option) (f : task)
raise Shutdown
(** Run this task, now. Must be called from a worker. *)
let run_task_now_ (self : state) ~runner (w : worker_state) ~name ~ls task :
let run_task_now_ (self : state) ~runner (w : worker_state) ~ls task :
unit =
(* Printf.printf "run task now (%d)\n%!" (Thread.id @@ Thread.self ()); *)
let (AT_pair (before_task, after_task)) = self.around_task in
w.cur_ls := ls;
let _ctx = before_task runner in
w.cur_span <- Tracing_.enter_span name;
let[@inline] exit_span_ () =
Tracing_.exit_span w.cur_span;
w.cur_span <- Tracing_.dummy_span
in
let on_suspend () =
exit_span_ ();
let[@inline] on_suspend () =
!(w.cur_ls)
in
let run_another_task ls ~name task' =
let run_another_task ls task' =
let w = find_current_worker_ () in
let ls' = Task_local_storage.Private_.Storage.copy ls in
schedule_task_ self w ~name ~ls:ls' task'
schedule_task_ self w ~ls:ls' task'
in
let resume ls k r =
let w = find_current_worker_ () in
schedule_task_ self w ~name ~ls (fun () -> k r)
schedule_task_ self w ~ls (fun () -> k r)
in
(* run the task now, catching errors *)
@ -152,13 +143,12 @@ let run_task_now_ (self : state) ~runner (w : worker_state) ~name ~ls task :
let bt = Printexc.get_raw_backtrace () in
self.on_exn e bt);
exit_span_ ();
after_task runner _ctx;
w.cur_ls := Task_local_storage.Private_.Storage.dummy
let[@inline] run_async_ (self : state) ~name ~ls (f : task) : unit =
let[@inline] run_async_ (self : state) ~ls (f : task) : unit =
let w = find_current_worker_ () in
schedule_task_ self w ~name ~ls f
schedule_task_ self w ~ls f
(* TODO: function to schedule many tasks from the outside.
- build a queue
@ -204,7 +194,7 @@ let worker_run_self_tasks_ (self : state) ~runner w : unit =
match WSQ.pop w.q with
| Some task ->
try_wake_someone_ self;
run_task_now_ self ~runner w ~name:task.name ~ls:task.ls task.f
run_task_now_ self ~runner w ~ls:task.ls task.f
| None -> continue := false
done
@ -217,7 +207,7 @@ let worker_thread_ (self : state) ~(runner : t) (w : worker_state) : unit =
worker_run_self_tasks_ self ~runner w;
try_steal ()
and run_task task : unit =
run_task_now_ self ~runner w ~name:task.name ~ls:task.ls task.f;
run_task_now_ self ~runner w ~ls:task.ls task.f;
main ()
and try_steal () =
match try_to_steal_work_once_ self w with
@ -276,7 +266,7 @@ type ('a, 'b) create_args =
'a
(** Arguments used in {!create}. See {!create} for explanations. *)
let dummy_task_ = { f = ignore; ls = Task_local_storage.Private_.Storage.dummy ; name = "DUMMY_TASK" }
let dummy_task_ = { f = ignore; ls = Task_local_storage.Private_.Storage.dummy ; }
let create ?(on_init_thread = default_thread_init_exit_)
?(on_exit_thread = default_thread_init_exit_) ?(on_exn = fun _ _ -> ())
@ -301,7 +291,6 @@ let create ?(on_init_thread = default_thread_init_exit_)
{
pool_id_;
thread = dummy;
cur_span = Tracing_.dummy_span;
q = WSQ.create ~dummy:dummy_task_ ();
rng = Random.State.make [| i |];
cur_ls = ref Task_local_storage.Private_.Storage.dummy;
@ -326,7 +315,7 @@ let create ?(on_init_thread = default_thread_init_exit_)
let runner =
Runner.For_runner_implementors.create
~shutdown:(fun ~wait () -> shutdown_ pool ~wait)
~run_async:(fun ~name ~ls f -> run_async_ pool ~name ~ls f)
~run_async:(fun ~ls f -> run_async_ pool ~ls f)
~size:(fun () -> size_ pool)
~num_tasks:(fun () -> num_tasks_ pool)
()

View file

@ -148,9 +148,9 @@ let add_child_ ~protect (self : _ t) (child : _ t) =
let k_current_fiber : any option Task_local_storage.key =
Task_local_storage.new_key ~init:(fun () -> None) ()
let spawn_ ?name ~on (f : _ -> 'a) : 'a t =
let spawn_ ~on (f : _ -> 'a) : 'a t =
let id = Handle.generate_fresh () in
let res, _promise = Fut.make ?name () in
let res, _promise = Fut.make () in
let fib =
{
state = A.make @@ Alive { children = FM.empty; on_cancel = [] };
@ -172,17 +172,17 @@ let spawn_ ?name ~on (f : _ -> 'a) : 'a t =
resolve_as_failed_ fib ebt
in
Runner.run_async on ?name run;
Runner.run_async on run;
fib
let[@inline] spawn_top ?name ~on f : _ t = spawn_ ?name ~on f
let[@inline] spawn_top ~on f : _ t = spawn_ ~on f
let spawn_link ?name ~protect f : _ t =
let spawn_link ~protect f : _ t =
match Task_local_storage.get k_current_fiber with
| None -> failwith "Fiber.spawn_link: must be run from inside a fiber."
| Some (Any parent) ->
let child = spawn_ ?name ~on:parent.runner f in
let child = spawn_ ~on:parent.runner f in
add_child_ ~protect parent child;
child

View file

@ -55,12 +55,12 @@ val on_result : 'a t -> 'a callback -> unit
with the result. If the fiber is done already then the
callback is invoked immediately with its result. *)
val spawn_top : ?name:string -> on:Runner.t -> (unit -> 'a) -> 'a t
val spawn_top : on:Runner.t -> (unit -> 'a) -> 'a t
(** [spawn_top ~on f] spawns a new (toplevel) fiber onto the given runner.
This fiber is not the child of any other fiber: its lifetime
is only determined by the lifetime of [f()]. *)
val spawn_link : ?name:string -> protect:bool -> (unit -> 'a) -> 'a t
val spawn_link : protect:bool -> (unit -> 'a) -> 'a t
(** [spawn_link ~protect f] spawns a sub-fiber [f_child]
from a running fiber [parent].
The sub-fiber [f_child] is attached to the current fiber and fails

View file

@ -135,7 +135,7 @@ let for_ ?chunk_size n (f : int -> int -> unit) : unit =
let len_range = min chunk_size (n - offset) in
assert (offset + len_range <= n);
run ~name:"" (fun () -> task_for ~offset ~len_range);
run (fun () -> task_for ~offset ~len_range);
i := !i + len_range
done
in

View file

@ -20,7 +20,7 @@ let fib ~on x : int Fut.t =
Fut.await t1 + Fut.await t2
)
in
Fut.spawn ~name:"fib" ~on (fun () -> fib_rec x)
Fut.spawn ~on (fun () -> fib_rec x)
(* NOTE: for tracy support
let () = Tracy_client_trace.setup ()