123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837# 2 "domain.ml"(**************************************************************************)(* *)(* OCaml *)(* *)(* KC Sivaramakrishnan, Indian Institute of Technology, Madras *)(* Stephen Dolan, University of Cambridge *)(* Tom Kelly, OCaml Labs Consultancy *)(* *)(* Copyright 2019 Indian Institute of Technology, Madras *)(* Copyright 2014 University of Cambridge *)(* Copyright 2021 OCaml Labs Consultancy Ltd *)(* *)(* All rights reserved. This file is distributed under the terms of *)(* the GNU Lesser General Public License version 2.1, with the *)(* special exception on linking described in the file LICENSE. *)(* *)(**************************************************************************)open!StdlibopenModes.Portable[@@@ocaml.flambda_o3]externalcpu_relax:unit->unit@@portable="%cpu_relax"externalruntime5:unit->bool@@portable="%runtime5"moduleObj_opt:sig@@portabletypetvalsome:'a->tvalis_some:t->boolvalfresh:unit->tarrayvalgrow_array:tarray->int->int->tarrayvalcompare_and_set:tarray->int->t->t->bool(** [unsafe_get obj] may only be called safely
if [is_some] is true.
[unsafe_get (some v)] is equivalent to
[Obj.obj (Obj.repr v)]. *)valunsafe_get:t->'aend=structtypet=Obj.tletnone=Obj.magic_portable(Obj.repr(ref0))letfresh()=Array.make7(Obj.magic_uncontendednone)let[@inline]somev=(* [Sys.opaque_identity] ensures that flambda does not look at the type of
* [x], which may be a [float] and conclude that the [st] is a float array.
* We do not want OCaml's float array optimisation kicking in here. *)Obj.repr(Sys.opaque_identityv)let[@inline]is_someobj=(obj!=Obj.magic_uncontendednone)let[@inline]unsafe_getobj=Obj.objobjlet[@inlinenever]grow_arraystidxsize=letreccompute_new_sizes=ifidx<sthenselsecompute_new_size(2*s+1)inletnew_size=compute_new_sizesizeinletnew_st=Array.makenew_size(Obj.magic_uncontendednone)inArray.blitst0new_st0size;new_stexternalcompare_and_set_field:tarray->int->t->t->bool@@portable="%atomic_cas_field"let[@inline]compare_and_setstidxoldnew_=(* In Flambda 2 there is a strict distinction between arrays and blocks. *)compare_and_set_field(Sys.opaque_identityst)idxoldnew_endmoduleRuntime_4=structmoduleDLS=structletstate=Obj.magic_portable(Atomic.make(Obj_opt.fresh()))letinit()=()type'akey=int*(unit->'a)Modes.Portable.tletkey_counter=Atomic.make0letnew_key?split_from_parent:_init_orphan=letidx=Atomic.fetch_and_addkey_counter1in(idx,{portable=init_orphan})(* If necessary, grow the current domain's local state array such that [idx]
* is a valid index in the array. *)let[@inline]recmaybe_growidx=letst=Atomic.get(Obj.magic_uncontendedstate)inletsz=Array.lengthstinifidx<szthenstelsebeginletnew_st=Obj_opt.grow_arraystidxszin(* We want a implementation that is safe with respect to
single-domain multi-threading: retry if the DLS state has
changed under our feet.
Note that the number of retries will be very small in
contended scenarios, as the array only grows, with
exponential resizing. *)ifAtomic.compare_and_set(Obj.magic_uncontendedstate)stnew_stthennew_stelsemaybe_growidxend(* Disable inlining to assure poll points are never inserted between grow
and set, which could cause us to drop the update. *)let[@inlinenever]set(typea)(idx,_init)(x:a)=(* Assures [idx] is in range. *)letst=maybe_growidxinArray.unsafe_setstidx(Obj_opt.somex)let[@inlinenever]init_idx(typea)idxold_obj(init:_->a)=letv:a=init()inletnew_obj=Obj_opt.somevin(* At this point, [st] or [st.(idx)] may have been changed
by another thread on the same domain.
If [st] changed, it was resized into a larger value,
we can just reuse the new value.
If [st.(idx)] changed, we drop the current value to avoid
letting other threads observe a 'revert' that forgets
previous modifications. *)letst=Atomic.get(Obj.magic_uncontendedstate)inifObj_opt.compare_and_setstidxold_objnew_objthenvelsebegin(* if st.(idx) changed, someone must have initialized
the key in the meantime. *)letupdated_obj=Array.unsafe_getstidxinifObj_opt.is_someupdated_objthen(Obj_opt.unsafe_getupdated_obj:a)elseassertfalseend(* Inlining is ok because it's safe to return a stale value. *)let[@inline]get(typea)((idx,init):akey):a=(* Assures [idx] is in range. *)letst=maybe_growidxinletobj=Array.unsafe_getstidxinifObj_opt.is_someobjthen(Obj_opt.unsafe_getobj:a)elseinit_idxidxobjinit.portableend(******** Callbacks **********)(* first spawn, domain startup and at exit functionality *)letfirst_domain_spawned=Atomic.makefalseletfirst_spawn_function=ref(fun()->())letbefore_first_spawnf=ifAtomic.getfirst_domain_spawnedthenraise(Invalid_argument"first domain already spawned")elsebeginletold_f=!first_spawn_functioninletnew_f()=old_f();f()infirst_spawn_function:=new_fendletat_exit_key=DLS.new_key(fun()->{portable=(fun()->())})letat_exitf=letold_exit:unit->unit=(DLS.getat_exit_key).portableinletnew_exit()=(* The domain termination callbacks ([at_exit]) are run in
last-in-first-out (LIFO) order in order to be symmetric with the domain
creation callbacks ([at_each_spawn]) which run in first-in-fisrt-out
(FIFO) order. *)f();old_exit()inDLS.setat_exit_key{portable=new_exit}letdo_at_exit()=letf:unit->unit=(DLS.getat_exit_key).portableinf()(* Unimplemented functions *)letnot_implemented()=failwith"Multi-domain functionality not supported in runtime4"type!'at:valuemodportablecontendedwith'atypeid=intletspawn_=not_implemented()letjoin_=not_implemented()letget_id_=not_implemented()letself()=0letis_main_domain()=trueletrecommended_domain_count()=1letmax_domain_count=1letself_index()=0endmoduleRuntime_5=structmoduleRaw=struct(* Low-level primitives provided by the runtime *)typet=privateint(* The layouts of [state] and [term_sync] are hard-coded in
[runtime/domain.c] *)type'astate=|Running|Finishedof('a,exn)result[@warning"-unused-constructor"]type'aterm_sync:valuemodportablecontendedwith'a={(* protected by [mut] *)mutablestate:'astate[@warning"-unused-field"];mut:Mutex.t;cond:Condition.t;}[@@unsafe_allow_any_mode_crossing]externalspawn:(unit->'a)@portableonce->'aterm_sync->t@@portable="caml_domain_spawn"externalself:unit->t@@portable="caml_ml_domain_id"[@@noalloc]externalget_recommended_domain_count:unit->int@@portable="caml_recommended_domain_count"[@@noalloc]externalget_max_domain_count:unit->int@@portable="caml_max_domain_count"[@@noalloc]endtypeid=Raw.ttype'at={domain:Raw.t;term_sync:'aRaw.term_sync;}moduleDLS=structtypedls_state=Obj_opt.tarrayexternalget_dls_state:unit->dls_state@@portable="%dls_get"externalset_dls_state:dls_state->unit@@portable="caml_domain_dls_set"[@@noalloc]externalcompare_and_set_dls_state:dls_state->dls_state->bool@@portable="caml_domain_dls_compare_and_set"[@@noalloc]letinit()=letst=Obj_opt.fresh()inset_dls_statesttype'akey=int*(unit->'a)Modes.Portable.tletkey_counter=Atomic.make0typekey_initializer:valuemodcontendedportable=KI:'akey*('a->(unit->'a)@portableonce)@@portable->key_initializer[@@unsafe_allow_any_mode_crossing"CR with-kinds"]typekey_initializer_list=key_initializerlistletparent_keys=Atomic.make([]:key_initializer_list)letrecadd_parent_keyki=letl=Atomic.getparent_keysinifnot(Atomic.compare_and_setparent_keysl(ki::l))thenadd_parent_keykiletnew_key?split_from_parentinit_orphan=letidx=Atomic.fetch_and_addkey_counter1inletk=idx,{portable=init_orphan}inbeginmatchsplit_from_parentwith|None->()|Somesplit->add_parent_key(KI(k,split))end;k(* If necessary, grow the current domain's local state array such that [idx]
* is a valid index in the array. *)let[@inline]recmaybe_growidx=letst=get_dls_state()inletsz=Array.lengthstinifidx<szthenstelsebeginletnew_st=Obj_opt.grow_arraystidxszin(* We want a implementation that is safe with respect to
single-domain multi-threading: retry if the DLS state has
changed under our feet.
Note that the number of retries will be very small in
contended scenarios, as the array only grows, with
exponential resizing. *)ifcompare_and_set_dls_statestnew_stthennew_stelsemaybe_growidxend(* Disable inlining to assure poll points are never inserted between grow
and set, which could cause us to drop the update. *)let[@inlinenever]set(typea)(idx,_init)(x:a)=(* Assures [idx] is in range. *)letst=maybe_growidxinArray.unsafe_setstidx(Obj_opt.somex)let[@inlinenever]init_idx(typea)idxold_obj(init:_->a)=letv:a=init()inletnew_obj=Obj_opt.somevin(* At this point, [st] or [st.(idx)] may have been changed
by another thread on the same domain.
If [st] changed, it was resized into a larger value,
we can just reuse the new value.
If [st.(idx)] changed, we drop the current value to avoid
letting other threads observe a 'revert' that forgets
previous modifications. *)letst=get_dls_state()inifObj_opt.compare_and_setstidxold_objnew_objthenvelsebegin(* if st.(idx) changed, someone must have initialized
the key in the meantime. *)letupdated_obj=Array.unsafe_getstidxinifObj_opt.is_someupdated_objthen(Obj_opt.unsafe_getupdated_obj:a)elseassertfalseend(* Inlining is ok because it's safe to return a stale value. *)let[@inline]get(typea)((idx,init):akey):a=(* Assures [idx] is in range. *)letst=maybe_growidxinletobj=Array.unsafe_getstidxinifObj_opt.is_someobjthen(Obj_opt.unsafe_getobj:a)elseinit_idxidxobjinit.portabletypekey_value:valuemodportablecontended=KV:'akey*(unit->'a)@@portable->key_value[@@unsafe_allow_any_mode_crossing"CR with-kinds"]letget_initial_keys():key_valuelist=List.map(* [v] is applied exactly once in [set_initial_keys] *)(fun(KI(k,split))->letv=Obj.magic_many(split(getk))|>Obj.magic_portableinKV(k,v))(Atomic.getparent_keys:key_initializer_list)letset_initial_keys(l:key_valuelist)=List.iter(fun(KV(k,v))->setk(v()))lend(******** Identity **********)letget_id{domain;_}=domainletself()=Raw.self()letis_main_domain()=(self():>int)=0externalself_index:unit->int#@@portable="%domain_index"[@@noalloc]externaltag_int:int#->int@@portable="%tag_int"let[@inline]self_index()=tag_int(self_index())(******** Callbacks **********)(* first spawn, domain startup and at exit functionality *)letfirst_domain_spawned=Atomic.makefalseletfirst_spawn_function=Obj.magic_portable(ref(fun()->()))letbefore_first_spawnf=ifAtomic.getfirst_domain_spawnedthenraise(Invalid_argument"first domain already spawned")elsebeginletold_f=!first_spawn_functioninletnew_f()=old_f();f()infirst_spawn_function:=new_fendletdo_before_first_spawn()=ifnot(Atomic.getfirst_domain_spawned)thenbeginAtomic.setfirst_domain_spawnedtrue;letfirst_spawn_function=Obj.magic_uncontendedfirst_spawn_functionin!first_spawn_function();(* Release the old function *)first_spawn_function:=(fun()->())endletat_exit_key=DLS.new_key(fun()->{portable=(fun()->())})letat_exitf=letold_exit:unit->unit=(DLS.getat_exit_key).portableinletnew_exit()=f();old_exit()inDLS.setat_exit_key{portable=new_exit}letdo_at_exit()=letf:unit->unit=(DLS.getat_exit_key).portableinf()(******* Creation and Termination ********)letspawnf=do_before_first_spawn();letdls_keys=DLS.get_initial_keys()in(* [term_sync] is used to synchronize with the joining domains *)letterm_sync=Raw.{state=Running;mut=Mutex.create();cond=Condition.create()}inletbody()=matchDLS.init();DLS.set_initial_keysdls_keys;letres=f()inreswith(* Run the [at_exit] callbacks when the domain computation either
terminates normally or exceptionally. *)|res->(* If the domain computation terminated normally, but the
[at_exit] callbacks raised an exception, then return the
exception. *)do_at_exit();res|exceptionexn->(* If both the domain computation and the [at_exit] callbacks
raise exceptions, then ignore the exception from the
[at_exit] callbacks and return the original exception. *)(trydo_at_exit()with_->());raiseexninletdomain=Raw.spawnbodyterm_syncin{domain;term_sync}letjoin{term_sync;_}=letopenRawinletrecloop()=matchterm_sync.statewith|Running->Condition.waitterm_sync.condterm_sync.mut;loop()|Finishedres->resinmatchMutex.protectterm_sync.mutloopwith|Okx->x|Errorex->raiseexletrecommended_domain_count=Raw.get_recommended_domain_countletmax_domain_count=Raw.get_max_domain_count()endmoduletypeS=sigmoduleDLS:sigtype'akey=int*(unit->'a)Modes.Portable.tvalnew_key:?split_from_parent:('a->(unit->'a)@portableonce)@portable->(unit->'a)@portable->'akey@@portablevalget:'akey->'a@@portablevalset:'akey->'a->unit@@portablevalinit:unit->unitendtype!'at:valuemodportablecontendedwith'avalspawn:(unit->'a)@portableonce->'at@@portablevaljoin:'at->'a@@portabletypeid=privateintvalget_id:'at->id@@portablevalself:unit->id@@portablevalis_main_domain:unit->bool@@portablevalrecommended_domain_count:unit->int@@portablevalmax_domain_count:intvalself_index:unit->int@@portablevalbefore_first_spawn:(unit->unit)->unit@@nonportablevalat_exit:(unit->unit)@portable->unit@@portablevaldo_at_exit:unit->unit@@nonportableendletruntime_4_impl=(moduleRuntime_4:S)letruntime_5_impl=(moduleRuntime_5:S)letimpl=ifruntime5()thenruntime_5_implelseruntime_4_implmoduleM:S=(valimpl)includeMmoduleTLS0=structtypetls_state=Obj_opt.tarrayexternalget_tls_state:unit->tls_state@@portable="%tls_get"[@@noalloc]externalset_tls_state:tls_state->unit@@portable="caml_domain_tls_set"[@@noalloc]letget_tls_state=ifruntime5()thenget_tls_stateelsefun[@inline]()->letstate=get_tls_state()inifObj.is_block(Obj.reprstate)thenstateelse[||]type'akey='aDLS.keyletkey_counter=Atomic.make0typekey_initializer:valuemodcontendedportable=KI:'akey*('a->(unit->'a)@portableonce)@@portable->key_initializer[@@unsafe_allow_any_mode_crossing"CR with-kinds"]typekey_initializer_list=key_initializerlistletparent_keys=Atomic.make([]:key_initializer_list)letrecadd_parent_keyki=letl=Atomic.getparent_keysinifnot(Atomic.compare_and_setparent_keysl(ki::l))thenadd_parent_keykiletnew_key?split_from_parentinit_orphan=letidx=Atomic.fetch_and_addkey_counter1inletk=idx,{Modes.Portable.portable=init_orphan}inbeginmatchsplit_from_parentwith|None->()|Somesplit->add_parent_key(KI(k,split))end;k(* If necessary, grow the current domain's local state array such that [idx]
* is a valid index in the array. *)let[@inline]maybe_growidx=letst=get_tls_state()inletsize=Array.lengthstinifidx<sizethenstelsebeginletnew_st=Obj_opt.grow_arraystidxsizeinset_tls_statenew_st;new_stendlet[@inline]set(typea)(idx,_init)(x:a)=(* Assures [idx] is in range. *)letst=maybe_growidxinArray.unsafe_setstidx(Obj_opt.somex)let[@inlinenever]init_idx(typea)idx(init:_->a)=letv:a=init()inletnew_obj=Obj_opt.somevinletst=get_tls_state()inArray.unsafe_setstidxnew_obj;vlet[@inline]get(typea)((idx,init):akey):a=(* Assures [idx] is in range. *)letst=maybe_growidxinletobj=Array.unsafe_getstidxinifObj_opt.is_someobjthen(Obj_opt.unsafe_getobj:a)elseinit_idxidxinit.portabletypekey_value:valuemodportablecontended=KV:'akey*(unit->'a)@@portable->key_value[@@unsafe_allow_any_mode_crossing"CR with-kinds"]modulePrivate=structtypekeys=key_valuelistletinit()=letst=Obj_opt.fresh()inset_tls_statestletget_initial_keys():key_valuelist=List.map(* [v] is applied exactly once in [set_initial_keys] *)(fun(KI(k,split))->letv=Obj.magic_many(split(getk))|>Obj.magic_portableinKV(k,v))(Atomic.getparent_keys:key_initializer_list)letset_initial_keys(l:key_valuelist)=List.iter(fun(KV(k,v))->setk(v()))lendendmoduleTick=structmoduletypeS=sig@@portabletypet:mutable_datamodexternal_globalvalacquire:interval_usec:int->t@uniquevalrelease:t@unique->unitendmoduleRuntime4=structtypet=|letfail()=failwith"[Domain.Tick] not supported in runtime4"letacquire~interval_usec:_=fail()letrelease=function|(_:t)->.endmoduleRuntime5=structmoduleRegistry:sig@@portabletypet:sync_datatypeinner:mutable_datavalcreate:unit->tvalprotect:t->(inner->'r@contendedportable)@localonceportable->'r@contendedportable(** These two functions return the new min interval *)valadd:inner->int->intvalremove:inner->int->intor_nullend=struct(* NOTE this is extremely un-optimized; we assume that ticks are acquired
and released relatively infrequently so the performance of registry
modification doesn't matter. *)moduleInner=struct(* It would be better for this to be a decent min-heap, but there is not
one around that is convenient to use (and see above note about this
not being tight-loop). *)includeMap.MakePortable(Int)externalmagic_empty_stateless:('at[@local_opt])->('at[@local_opt])@stateless@@stateless="%identity"externalmagic_empty_read_write:('at[@local_opt])@immutable->('at[@local_opt])@@stateless="%identity"letempty=magic_empty_statelessemptylet[@inline]empty()=magic_empty_read_writeemptyendtypet:sync_data={mutableinner:intInner.t(* A bag, mapping the interval to the number of requesters of ticks with
that interval *);mutex:Mutex.t}[@@unsafe_allow_any_mode_crossing"All accesses protected by mutex"]typeinner=tletcreate()={inner=Inner.empty();mutex=Mutex.create()}letprotecttf=(Mutex.protectt.mutex(fun()->{Modes.Portended.portended=ft})).portended(* Must be called from within [protect] *)letaddttick=letinner'=Inner.updatetick(function|None->Some1|Somei->Some(i+1))t.innerint.inner<-inner';Inner.min_bindinginner'|>fst(* Must be called from within [protect] *)letremovettick=letinner'=Inner.updatetick(function|None|Some0|Some1->None|Somei->Some(i-1))t.innerint.inner<-inner';ifInner.is_emptyinner'thenNullelseThis(Inner.min_bindinginner'|>fst)end(* NOTE: st_stubs.c relies on this being an int (and in particular not
scanned) *)typet=int(* One registry per recommended_domain_count.
If more than recommended_domain_count domains are spawned (which in
practice we never do in OxCaml), multiple domains share a registry. This
is fine since we have to have synchronization for systhreads anyway
*)letregistry=(* CR ocaml-5.4: This should be an iarray *)Array.init(recommended_domain_count())(fun_->Registry.create())letlocal_registry()=(* Safety: modulo ensures this is in bounds *)Array.unsafe_get(* Safety: Array is never mutated after creation *)(Obj.magic_uncontendedregistry)(self_index()modrecommended_domain_count())externalset_tick_interval_usec:(int[@untagged])->(unit[@untagged])@@portable="caml_domain_set_tick_interval_usec_bytecode""caml_domain_set_tick_interval_usec"letacquire~interval_usec=ifinterval_usec<=0theninvalid_arg"Tick.acquire: interval must be strictly positive";Registry.protect(local_registry())(funregistry->letinterval=Registry.addregistryinterval_usecinset_tick_interval_usecinterval);interval_usecletreleaseinterval_usec=Registry.protect(local_registry())(funregistry->(* Note that the calls to [set_tick_interval_usec] can't raise here, as
we're neither setting the requested interval to a value we haven't
successfully set it to before, nor are we starting the tick
thread. *)matchRegistry.removeregistryinterval_usecwith|Null->set_tick_interval_usec0|Thisinterval->set_tick_interval_usecinterval)endletimpl=ifruntime5()then(moduleRuntime5:S)else(moduleRuntime4:S)include(valimpl:S)let()=Callback.Safe.register"Domain.Tick.acquire"acquirelet()=Callback.Safe.register"Domain.Tick.release"releaseexternaleffective_interval_usec_prim:(unit[@untagged])->(int[@untagged])@@portable="caml_effective_tick_interval_usec_bytecode""caml_effective_tick_interval_usec"[@@noalloc]leteffective_interval_usec()=matcheffective_interval_usec_prim()with|0->Null|n->ThisnendmoduleSafe=struct(* Note the exposed signature of [get] and [set] add modes for safety. *)moduleDLS=DLSmoduleTLS=TLS0letspawnf=lettls_keys=TLS.Private.get_initial_keys()inspawn(fun()->TLS.Private.init();TLS.Private.set_initial_keystls_keys;f())[@nontail]letat_exit=at_exitendmoduleTLS=structmodulePrivate=TLS0.Privatetype'akey='aTLS0.keyletnew_key?split_from_parentf=letsplit_from_parent=matchsplit_from_parentwith|None->None|Somesplit_from_parent->Some(Obj.magic_portable(funx->Obj.magic_portable(fun()->split_from_parentx)))inTLS0.new_key?split_from_parent(Obj.magic_portablef);;letget=TLS0.getletset=TLS0.setletinit=TLS0.Private.initendmoduleDLS=structtype'akey='aSafe.DLS.keyletnew_key?split_from_parentf=letsplit_from_parent=matchsplit_from_parentwith|None->None|Somesplit_from_parent->Some(Obj.magic_portable(funx->Obj.magic_portable(fun()->split_from_parentx)))inDLS.new_key?split_from_parent(Obj.magic_portablef);;letget=DLS.getletset=DLS.setletinit=DLS.initendletspawnf=Safe.spawn(Obj.magic_portablef)letat_exitf=Safe.at_exit(Obj.magic_portablef)let()=DLS.init()let()=TLS.init()let_=Stdlib.do_domain_local_at_exit:=do_at_exit