123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903(* This module adds support for running Async code in Ecaml.
We change the way the scheduler works. The Async scheduler runs in its own thread, but
all Async cycles now run within the main emacs thread. When the scheduler would run a
cycle, it instead sends a packet to a socket that the emacs main thread listens to
(which notifies emacs that it should run a cycle), and waits for the cycle to be run.
This way, whenever we run Ecaml code, we have both the Emacs [active_env] and the Async
lock. This way, it is always safe to call Ecaml functions from Async, and to modify
Async data structures from Ecaml. *)moduleEcaml_filename=Filenameopen!Coreopen!ImportmoduleIvar=Async.IvarmoduleMutex=Error_checking_mutexmoduleThread=Caml_threads.ThreadmoduleTime=Time_unixmoduleUnix=Core_unixmoduleScheduler=Async_unix.Async_unix_private.Raw_schedulerletmessage_s=message_smoduleQ=structletecaml_async_take_lock_do_cycle="ecaml-async-take-lock-do-cycle"|>Symbol.internendmoduleScheduler_status=structtypet=|Uninitialized|Running|Stopped[@@derivingsexp]letstatus=refUninitializedendmoduleCycle_report=structletcycles:Time_ns.Span.tlistref=ref[]letmeasuring=reffalseletreport_cycletime=if!measuringthencycles:=time::!cyclesletgenerate_report()=letopenAsyncinmessage"Collecting 10 seconds of cycle data...";measuring:=true;let%bind()=Clock_ns.after(sec_ns10.)inmeasuring:=false;letsamples=!cyclesincycles:=[];letbuffer=Buffer.find_or_create~name:"cycle report"inlet%bind()=Selected_window.switch_to_bufferbufferinList.itersamples~f:(funsample->Point.insert(sprintf!"%{Time_ns.Span}\n"sample));letavg=letsum=List.fold_left~init:Time_ns.Span.zero~f:Time_ns.Span.(+)samplesinTime_ns.Span.(sum/Float.of_int(List.lengthsamples))inPoint.insert(sprintf!"Average cycle time: %{Time_ns.Span}\n"avg);return();;end(** [Thread_safe_sleeper] is a thread-safe data structure for use by threads holding
the Async lock. The usage pattern is that one thread calls [blocking_sleep], which
causes it to release the Async lock (and OCaml lock) and block. Later, another
thread calls [wake_up], after which the blocked thread wakes up and reacquires
the locks. *)moduleThread_safe_sleeper:sigtypet[@@derivingsexp_of]valcreate:unit->t(** [blocking_sleep] assumes [Scheduler.am_holding_lock scheduler]. It unlocks the
scheduler before sleeping, and re-locks the scheduler as soon as it wakes up. *)valblocking_sleep:t->unitvalwake_up:t->unitend=structtypet={mutex:(Mutex.t[@sexp.opaque]);wake_up:(Condition.t[@sexp.opaque]);scheduler:Scheduler.t}[@@derivingsexp_of]letcreate()={mutex=Mutex.create();wake_up=Condition.create();scheduler=Scheduler.t()};;letcritical_sectiont~f=assert(Scheduler.am_holding_lockt.scheduler);Mutex.critical_sectiont.mutex~f;;letblocking_sleept=(* We unlock the Async lock while in a critical section rather than before
the critical section to avoid a race in which:
- Thread 1 requests service from Thread 2
- Thread 1 calls [blocking_sleep]
- Thread 1 unlocks the Async lock
- Thread 2 locks the Async lock
- Thread 2 performs service and calls [wake_up]
- Thread 2 unlocks the Async lock
- Thread 1 calls [wait], and is then stuck. *)critical_sectiont~f:(fun()->Scheduler.unlockt.scheduler;Condition.waitt.wake_upt.mutex);Scheduler.lockt.scheduler;;letwake_upt=critical_sectiont~f:(fun()->Condition.broadcastt.wake_up)end(** The Cycle_requester is a mechanism for requesting emacs to run a cycle by writing a
byte along a socket. Emacs passes this byte on to the process filter we define, which
runs a cycle in the emacs thread.
We maintain the invariant that there is at most one byte waiting to be read at a
time. *)moduleCycle_requester:sigtypetvalbyte_was_probably_lost:t->unitvalcreate:unit->tvalrequest_cycle:t->unitvalregister_cycle_handler:t->(unit->unit)->unitvalshutdown:t->unitend=structtypet={mutableclient_process:Process.toption;mutableexists_unread_byte:bool;mutableserver_process:Process.toption;write_to_request_cycle:Unix.File_descr.t}letbyte_was_probably_lostt=t.exists_unread_byte<-falseletcreate()=letwrite_to_request_cycle=Unix.socket~domain:PF_UNIX~kind:SOCK_STREAM~protocol:0()in(* If the scheduler blocks trying to write to this socket, emacs will deadlock. Making
the socket nonblocking prevents this. *)Unix.set_nonblockwrite_to_request_cycle;{client_process=None;exists_unread_byte=false;server_process=None;write_to_request_cycle};;letrequest_cyclet=assert(Scheduler.am_holding_lock(Scheduler.t()));(* Ensure we write at most 1 byte to the socket between runs of the cycle handler.
[request_cycle] might be called multiple times. In extreme cases, writing more
than one byte to the socket could lead to the socket's buffer filling up and
[write] blocking, deadlocking emacs. *)ifnott.exists_unread_bytethen((* This code is based on the [Async_unix.Interruptor] idiom for notifying the
interruptor pipe, which uses a nonblocking file descriptor, doesn't give up the
OCaml lock and raises if the write would block. We don't explicitly handle
EWOULDBLOCK and EAGAIN, since we already discard all exceptions anyways. *)tryignore(Unix.write_assume_fd_is_nonblockingt.write_to_request_cycle(Bytes.of_string"\x05"):int);t.exists_unread_byte<-truewith|_->(* We ignore exceptions here, because the network socket may be closed, e.g. when
Emacs is shutting down, and we don't want to fail in that case. *)());;letwith_current_dirdir~f=letsaved_dir=Unix.getcwd()inUnix.chdirdir;Exn.protect~f~finally:(fun()->Unix.chdirsaved_dir);;letregister_cycle_handlertrun_cycle=lettmpdir=Ecaml_filename.to_directory(Option.value(System.getenv~var:"TMPDIR")~default:"/tmp")in(* If [String.length tmpdir > 108], then creating a unix socket at that path fails
with "Service name too long". To avoid this, we chdir and create the socket using
a relative path. *)with_current_dirtmpdir~f:(fun()->letsocket_path=".ecaml."^(Unix.getpid()|>Pid.to_string)inletserver_process=Process.create_unix_network_process()~name:"Async scheduler"~socket_path~filter:(funclient_process_->assert(Scheduler.am_holding_lock(Scheduler.t()));t.exists_unread_byte<-false;(matcht.client_processwith|Some_->()|None->t.client_process<-Someclient_process;Process.set_query_on_exitclient_processfalse);run_cycle())inProcess.set_query_on_exitserver_processfalse;Unix.connectt.write_to_request_cycle~addr:(ADDR_UNIXsocket_path);Unix.unlinksocket_path;t.server_process<-Someserver_process);;letshutdownt=Option.itert.client_process~f:Process.kill;Option.itert.server_process~f:Process.kill;;end(* A [Pending_emacs_call.t] is a function that should be run outside of any Async job and
without the Async lock, but reports its result back into Async. To ensure that pending
emacs calls are run in a timely manner, we run them whenever we run Async cycles, and
we request Async cycles whenever we enqueue a pending emacs call. *)modulePending_emacs_call=structtype'acall={f:unit->'a;result:('a,exn)Result.tIvar.t;running_in_background:Source_code_position.toption}typet=T:'acall->tendmodulePending_foreground_block_on_async=structtypet={context:Sexp.tLazy.toption;execution_context:Async.Execution_context.t;f:unit->unitAsync.Deferred.t;here:Source_code_position.t}endtypet={(* [am_running_async_cycle] is set to [true] while we're running an Async cycle.
During an Async cycle, we avoid running nested Async cycles or
[block_on_async]s. *)mutableam_running_async_cycle:bool;cycle_done_sleeper:Thread_safe_sleeper.t;cycle_requester:Cycle_requester.t;emacs_thread_id:int;mutableexceptions_raised_outside_emacs_env:exnlist;mutablekeepalive_timer:Timer.toption;mutablelast_cycle_finished_at:Time_ns.t;scheduler:Scheduler.t;mutablepending_emacs_calls:Pending_emacs_call.tQueue.t;mutablepending_foreground_block_on_asyncs:Pending_foreground_block_on_async.tQueue.t}lett={am_running_async_cycle=false;cycle_done_sleeper=Thread_safe_sleeper.create();cycle_requester=Cycle_requester.create();emacs_thread_id=Thread.(id(self()));exceptions_raised_outside_emacs_env=[];keepalive_timer=None;last_cycle_finished_at=Time_ns.epoch;scheduler=Scheduler.t();pending_emacs_calls=Queue.create();pending_foreground_block_on_asyncs=Queue.create()};;letrun_pending_emacs_calls()=lethas_work_to_do=not(Queue.is_emptyt.pending_emacs_calls)inifhas_work_to_dothen(letpending_calls=t.pending_emacs_callsint.pending_emacs_calls<-Queue.create();Queue.iterpending_calls~f:(fun(Pending_emacs_call.Tpending_emacs_call)->Scheduler.unlockt.scheduler;letrun_job()=Result.try_withpending_emacs_call.finletresult=matchpending_emacs_call.running_in_backgroundwith|Somelocation->Background.Private.mark_running_in_backgroundlocation~f:run_job|None->(* If we're in this branch, that means this job was enqueued in the foreground,
and so the foreground is blocking on the result of this job. So we make sure
this job sees that it is running in the foreground. *)Background.Private.mark_running_in_foreground~f:run_jobinScheduler.lockt.scheduler;Ivar.fillpending_emacs_call.resultresult));has_work_to_do;;moduleBlock_on_async=structmoduleContext_backtrace=structmoduleFrame=structtypet={here:Source_code_position.t;context:Sexp.tLazy.t;created_at:Time_ns_unix.topaque_in_test}[@@derivingsexp_of]endtypet=Frame.tlist[@@derivingsexp_of]endletcontext_backtrace:Context_backtrace.tref=ref[]letam_blocking_on_async()=not(List.is_empty!context_backtrace)letrecif_safe_run_pending()=assert(nott.am_running_async_cycle);if(not(am_blocking_on_async()))&¬(Queue.is_emptyt.pending_foreground_block_on_asyncs)then(letpending_foreground_block_on_asyncs=t.pending_foreground_block_on_asyncsint.pending_foreground_block_on_asyncs<-Queue.create();Queue.iterpending_foreground_block_on_asyncs~f:(fun{context;execution_context;f;here}->(* We ignore any error because [within_context] already sent it to
[execution_context]'s monitor. *)let(_:(unit,unit)result)=Scheduler.within_contextexecution_context(fun()->block_on_asynchere?contextf)in()))(* When the scheduler requests an Async cycle, [in_emacs_have_lock_do_cycle] runs the
cycle inside the emacs thread and notifies the scheduler when it is finished. *)andin_emacs_have_lock_do_cycle()=if(not(Value.Expert.have_active_env()))||not(Scheduler.am_holding_lockt.scheduler)thenraise_s[%sexp"[in_emacs_have_lock_do_cycle] should only be called by emacs"];(* If we are already running an Async cycle, then we can't start a new one, so we do
nothing. We can reach here with [t.am_running_async_cycle = true] if Ecaml calls a
blocking Elisp function without using [run_outside_async]. We are in the middle of a
long, possibly unending, code transition in which we are wrapping such calls with
[run_outside_async]. *)ifnott.am_running_async_cyclethen(ifdebugthenDebug.eprint_s[%message"running a cycle"~time:(Time.now():Time.t)];if_safe_run_pending();List.itert.exceptions_raised_outside_emacs_env~f:(funexn->message_s[%sexp(exn:exn)]);t.exceptions_raised_outside_emacs_env<-[];lettime=Time.now()inExn.protect~f:(fun()->Async.Unix.Private.Wait.check_all();letrecrun_cyclesmax_cycles=(* Pending emacs calls may have been enqueued from outside of Async. Run them so
their deferreds get filled. *)letran_pending_calls=run_pending_emacs_calls()int.am_running_async_cycle<-true;letold_execution_context=Async_kernel.Async_kernel_scheduler.current_execution_context()inExn.protect~f:(fun()->Async_kernel.Async_kernel_scheduler.Private.(run_cycle(t())))~finally:(fun()->(* Restore the execution context effective before running cycles. This
prevents background jobs from raising exceptions to random monitors,
because the execution context of whichever job happened to run last would
have been left intact. *)Async_kernel.Async_kernel_scheduler.Private.(set_execution_context(t())old_execution_context);t.am_running_async_cycle<-false);ifmax_cycles>0&&(ran_pending_calls||Scheduler.num_pending_jobs()>0)thenrun_cycles(max_cycles-1)in(* 5 was chosen as an arbitrary limit to prevent the emacs toplevel from being
starved if an Async job misbehaves. *)run_cycles5)~finally:(fun()->t.last_cycle_finished_at<-Time_ns.now();ifdebugthenDebug.eprint_s[%message"cycle took"~time:(Time.diff(Time.now())time:Time.Span.t)];Thread_safe_sleeper.wake_upt.cycle_done_sleeper))andblock_on_async:typea._->?context:_->?for_testing_allow_nested_block_on_async:_->(unit->aAsync.Deferred.t)->a=funhere?context?(for_testing_allow_nested_block_on_async=false)f->assert(Scheduler.am_holding_lockt.scheduler);Ref.set_temporarilycontext_backtrace(iffor_testing_allow_nested_block_on_asyncthen[]else{here;context=Option.valuecontext~default:(lazy[%message]);created_at=Time_ns.now()}::!context_backtrace)~f:(fun()->ift.am_running_async_cyclethenraise_s[%message.omit_nil"Called [block_on_async] in the middle of an Async job!"(context_backtrace:Context_backtrace.tref)~profile_backtrace:(Nested_profile.Profile.backtrace():Sexp.tlistoption)];letrecrun_cycles_until_filleddeferred=ifRef.set_temporarilyProfile.should_profilefalse~f:Command.quit_requestedthenerror_s[%message"Blocking operation interrupted"]else(matchAsync.Deferred.peekdeferredwith|Someresult->result|None->(* [Thread.delay] gives the scheduler thread time to run before we run a
cycle. *)Scheduler.unlockt.scheduler;Thread.delay(Time.Span.of_us10.|>Time.Span.to_sec);Scheduler.lockt.scheduler;in_emacs_have_lock_do_cycle();run_cycles_until_filleddeferred)inletdeferred=Async.(Monitor.try_with~rest:`Log~extract_exn:true~run:`Schedulef>>|Or_error.of_exn_result)inletresult=run_cycles_until_filleddeferredinmatchresultwith|Okx->x|Errorerror->Error.raiseerror);;end(* [request_emacs_run_cycle] requests the emacs main thread to run a cycle. It hands over
the Async lock in the process. *)letrequest_emacs_run_cyclescheduler_thread_id()=assert(Scheduler.am_holding_lockt.scheduler);Cycle_requester.request_cyclet.cycle_requester;(* Async helper threads call [request_emacs_run_cycle], and we don't want those to
block; we want only the scheduler to block. *)ifThread.(id(self()))=scheduler_thread_idthen(letstart=Time_ns.now()inThread_safe_sleeper.blocking_sleept.cycle_done_sleeper;letdiff=Time_ns.diff(Time_ns.now())startinCycle_report.report_cyclediff);;letlock_async_during_module_initialization()=(* Acquire the Async lock, releasing it once module initialization is done. *)Ecaml_callback.(registerend_of_module_initialization)[%here]~should_run_holding_async_lock:false~f:(fun()->message_s[%message"Loaded Ecaml."];Scheduler.unlockt.scheduler);;letmax_inter_cycle_timeout=Time_ns.Span.secondletstart_scheduler()=match!Scheduler_status.statuswith|Stopped->raise_s[%sexp"Async has been shut down and cannot be restarted"]|Running->()|Uninitialized->assert(Scheduler.am_holding_lockt.scheduler);Scheduler_status.status:=Running;Async.Unix.Private.Wait.do_not_handle_sigchld();ifdebugthenDebug.eprint_s[%message"initializing async"[%here](Time.now():Time.t)];(* We hold the Async lock, so it should be impossible for the scheduler to try to run
a cycle. *)t.scheduler.have_lock_do_cycle<-Some(fun()->raise_s[%message"BUG in Async_ecaml"[%here]]);letscheduler_thread=Thread.create(fun()->matchScheduler.go()~raise_unhandled_exn:truewith|_->.|exceptionexn->(match!Scheduler_status.statuswith(* If we requested the scheduler to stop, this exception is expected. *)|Stopped->()|Running|Uninitialized->raiseexn))()in(* We set [have_lock_do_cycle] as early as possible so that the Async scheduler runs
cycles in the desired way, even if later parts of initialization raise. *)t.scheduler.have_lock_do_cycle<-Some(request_emacs_run_cycle(Thread.idscheduler_thread));Defun.defunQ.ecaml_async_take_lock_do_cycle[%here]~docstring:{|
For testing Async Ecaml.
This runs the same OCaml code that Aysnc Ecaml uses for running an Async cycle. It blocks
until it can acquire the Async lock and then run a cycle.
|}~interactive:No_arg(ReturnsValue.Type.unit)(letopenDefun.Let_syntaxinlet%map_open()=return()inBlock_on_async.in_emacs_have_lock_do_cycle());Cycle_requester.register_cycle_handlert.cycle_requesterBlock_on_async.in_emacs_have_lock_do_cycle;(* It is possible that emacs doesn't respond to a cycle request (maybe because emacs
is under high load). Instead of letting the scheduler block forever on a cycle that
will never run, we add a timer to ensure we run a cycle once per
[max_inter_cycle_timeout]. *)t.keepalive_timer<-Some(Timer.run_after[%here]max_inter_cycle_timeout~repeat:max_inter_cycle_timeout~name:("async-ecaml-keepalive-timer"|>Symbol.intern)~docstring:{|
Internal to Async Ecaml.
Periodically request an Async cycle.
|}~f:(fun()->tryifTime_ns.Span.(>=)(Time_ns.diff(Time_ns.now())t.last_cycle_finished_at)max_inter_cycle_timeoutthen(Cycle_requester.byte_was_probably_lostt.cycle_requester;Block_on_async.in_emacs_have_lock_do_cycle())with|exn->message_s[%sexp"Error in async keepalive timer",(exn:exn)]));(* The default [max_inter_cycle_timeout] is much smaller. Setting it to 1s reduces
load on emacs. *)Scheduler.set_max_inter_cycle_timeout(max_inter_cycle_timeout|>Time_ns.Span.to_span_float_round_nearest);(* [Async_unix] installs a handler for logging exceptions raised to try-with that has
already returned. That logs to stderr, which doesn't work well in Emacs. So we
install a handler that reports the error with [message_s]. *)(Async_kernel.Monitor.Expert.try_with_log_exn:=funexn->message_s[%message"Exception raised to [Monitor.try_with] that already returned."~_:(exn:exn)]);(* Async would normally deal with errors that reach the main monitor by printing to
stderr and then exiting 1. This would look like an emacs crash to the user, so we
instead output the error to the minibuffer. *)Async_kernel.Monitor.detach_and_iter_errorsAsync_kernel.Monitor.main~f:(funexn->ifValue.Expert.have_active_env()then(* We really want to see the error, so we inhibit quit while displaying it. *)Current_buffer.set_value_temporarilySyncCommand.inhibit_quittrue~f:(fun()->message_s[%sexp(exn:exn)])elset.exceptions_raised_outside_emacs_env<-exn::t.exceptions_raised_outside_emacs_env);;moduleExport=structmoduleClock=Async.Clockletdon't_wait_for=Async.don't_wait_formoduleAsync_process=Async.ProcessmoduleAsync=AsyncmoduleAsync_kernel=Async_kernelendmodulePrivate=structletblock_on_async=Block_on_async.block_on_asyncletenqueue_foreground_block_on_asynchere?context?(raise_exceptions_to_monitor=Async.Monitor.main)f=assert(Scheduler.am_holding_lockt.scheduler);Queue.enqueuet.pending_foreground_block_on_asyncs{context;execution_context=(* The current execution context's monitor may not be valid when [f] is run,
which might be long after that monitor has returned. *)Async.Execution_context.create_like(Scheduler.current_execution_contextt.scheduler)~monitor:raise_exceptions_to_monitor;f;here};;letrun_outside_asynchere?(allowed_in_background=false)f=ifnotallowed_in_backgroundthenBackground.assert_foreground~message:[%sexp"[run_outside_async] called unsafely in background"]here;letopenAsyncinDeferred.create(funresult->Queue.enqueuet.pending_emacs_calls(T{f;result;running_in_background=Background.currently_running_in_background()});(* We request an Async cycle to ensure the pending call is run in a timely
manner. *)Cycle_requester.request_cyclet.cycle_requester)>>|Result.ok_exn;;letrun_outside_async1here?allowed_in_backgroundfa=run_outside_asynchere?allowed_in_background(fun()->fa);;let()=Set_once.set_exnValue.Private.Block_on_async.set_once[%here]{f=Block_on_async.block_on_async~for_testing_allow_nested_block_on_async:false};Set_once.set_exnValue.Private.Enqueue_foreground_block_on_async.set_once[%here]{f=enqueue_foreground_block_on_async};Set_once.set_exnValue.Private.Run_outside_async.set_once[%here]{f=run_outside_async};;endmoduleExpect_test_config=structincludeAsync.Expect_test_configletrunf=Block_on_async.block_on_async[%here]~context:(lazy[%message"Expect_test_config.run"])f;;endmoduleExpect_test_config_allowing_nested_block_on_async=structincludeExpect_test_configletrunf=Block_on_async.block_on_async[%here]~for_testing_allow_nested_block_on_async:truef;;endletshutdown()=letstatus=!Scheduler_status.statusinScheduler_status.status:=Stopped;Cycle_requester.shutdownt.cycle_requester;(matcht.keepalive_timerwith|None->()|Sometimer->Timer.canceltimer;t.keepalive_timer<-None);matchstatuswith|Uninitialized|Stopped->()|Running->t.scheduler.have_lock_do_cycle<-Some(fun()->Scheduler.unlockt.scheduler;raise_s[%sexp"Async shutdown"]);;let()=start_scheduler();lock_async_during_module_initialization();Defun.defun_nullary_nil("ecaml-async-shutdown"|>Symbol.intern)[%here]~docstring:{|
Internal to Async Ecaml.
This shuts down the Async scheduler. It can not be restarted, so you will have to restart
Emacs afterwards.
|}~interactive:No_argshutdown;Defun.defun_nullary_nil("ecaml-async-generate-cycle-report"|>Symbol.intern)[%here]~docstring:{|
For testing Async Ecaml.
This runs Async cycles for 10s and then shows how long the cycles took.
|}~interactive:No_arg(fun()->Async.don't_wait_for(Cycle_report.generate_report()));letdefun_benchmark~name~f=Defun.defun_nullary(name|>Symbol.intern)[%here]~interactive:No_arg(Returns_deferredValue.Type.unit)(fun()->letopenAsyncinlet%maptime=f()inmessage_stime)indefun_benchmark~name:"ecaml-async-benchmark-small-pings"~docstring:{|
For testing Async Ecaml.
Run a benchmark that creates an Async TCP server and client and has the client ping the
server 100 times.
|}~f:Ecaml_bench.Bench_async_ecaml.benchmark_small_pings;defun_benchmark~name:"ecaml-async-benchmark-throughput"~docstring:{|
For testing Async Ecaml.
Run a benchmark that creates an Async TCP server and client and has the server send 100M
to the client.
|}~f:Ecaml_bench.Bench_async_ecaml.benchmark_throughput;Defun.defun_nullary("ecaml-async-test-block-forever"|>Symbol.intern)[%here]~interactive:No_arg~docstring:{|
For testing Async Ecaml.
Block on [Deferred.never ()] until you press [C-g].
|}(Returns_deferredValue.Type.unit)(fun()->message_s[%message"blocking forever -- press C-g to interrupt"];Async.Deferred.never());Defun.defun_nullary_nil("ecaml-async-test-execution-context-handling"|>Symbol.intern)[%here]~docstring:{|
For testing Async Ecaml.
Check aspects of Async Ecaml's handling of execution contexts.
|}~interactive:No_arg(fun()->letopenAsyncinlettest_passed=reftrueinletcheck_execution_context()=letexecution_context=Scheduler.current_execution_context()inifnot(phys_equalexecution_contextExecution_context.main)then(test_passed:=false;message_s[%message"Ecaml callback not running in main execution context"(execution_context:Execution_context.t)])incheck_execution_context();lettimer=Timer.run_after~repeat:(sec_ns0.1)[%here](sec_ns0.1)~f:check_execution_context~name:("check-execution-context-timer"|>Symbol.intern)~docstring:{|
Internal to Async Ecaml.
Periodically check that the execution context in which Async jobs run is
[Execution_context.main].
|}indon't_wait_for(let%map_ignored=Monitor.try_with(fun()->let%bind()=Clock.after(sec0.1)inlet%bind()=Clock.after(sec2.)inTimer.canceltimer;messagef"Execution-context test %s"(if!test_passedthen"passed"else"failed");return())in()));Defun.defun_nullary_nil("ecaml-async-test-in-thread-run"|>Symbol.intern)[%here]~docstring:{|
For testing Async Ecaml.
Call [In_thread.run] a number of times and report on its performance.
|}~interactive:No_arg(fun()->letopenAsyncindon't_wait_for(letopenDeferred.Let_syntaxinmessage_s[%message"testing"];letall_elapsed=ref[]inletlong_cutoff=sec_ns0.01inletrecloopi=ifi=0then(letall_elapsed=List.sort~compare:Time_ns.Span.compare(letx=!all_elapsedinall_elapsed:=[];x)inmessage_s[%message"test finished"(all_elapsed:Time_ns.Span.tlist)];return())else(letbefore=Time_ns.now()inlet%bind()=In_thread.run(fun()->Thread.yield())inletelapsed=Time_ns.diff(Time_ns.now())beforeinall_elapsed:=elapsed::!all_elapsed;ifTime_ns.Span.(>=)elapsedlong_cutoffthenmessage_s[%message"Slow [In_thread.run]"(elapsed:Time_ns.Span.t)];loop(i-1))inloop100));letdummy_key=Univ_map.Key.create~name:"dummy"[%sexp_of:int]inDefun.defun_nullary_nil("ecaml-async-test-execution-context-reset"|>Symbol.intern)[%here]~docstring:{|
Demonstrate a bug in Async_ecaml's handling of execution contexts.
In non-async Ecaml defuns, running some Elisp code that then calls back into Ecaml will
not preserve the current Async execution context.
|}~interactive:No_arg(fun()->(* The key-value pair starts out absent. *)assert(Option.is_none(Async.Scheduler.find_localdummy_key));letprint_data=Function.create_nullary[%here](fun()->matchAsync.Scheduler.find_localdummy_keywith|None->Echo_area.message"BUG: execution context is not preserved"|Somedata->Echo_area.message_s[%message"Execution context preserved"(data:int)])|>Function.to_valueinAsync.Scheduler.with_localdummy_key(Some42)~f:(fun()->(* The key-value pair is present. *)assert(Option.is_some(Async.Scheduler.find_localdummy_key));Form.list[Form.symbol("funcall"|>Symbol.intern);Form.quoteprint_data]|>Form.Blocking.eval_i));Defun.defun_nullary_nil("ecaml-async-test-enqueue-block-on-async"|>Symbol.intern)[%here]~docstring:{|
For testing Async Ecaml.
Test [Background.schedule_foreground_block_on_async]. This should block for a couple
seconds, and then open a buffer with a hello-world message.
|}~interactive:No_arg(fun()->letopenAsyncinBackground.don't_wait_for[%here](fun()->let%map()=Clock.after(sec1.)inBackground.schedule_foreground_block_on_async[%here](fun()->let%bind()=Clock.after(sec1.)inlet%bind()=Selected_window.switch_to_buffer(Buffer.find_or_create~name:"test buffer")inPoint.insert"Hello foreground world!";return())));;