Skip to content

Commit 9ff3108

Browse files
committed
Remove unnecessary wakeups and use requestIdleCallback
1 parent 138c78c commit 9ff3108

File tree

7 files changed

+78
-34
lines changed

7 files changed

+78
-34
lines changed

lib_eio_js/browser/dune

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
(library
22
(name eio_browser)
33
(public_name eio_browser)
4+
(foreign_stubs
5+
(language c)
6+
(names stubs))
7+
(js_of_ocaml (javascript_files runtime.js))
48
(libraries eio brr))

lib_eio_js/browser/eio_browser.ml

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -64,32 +64,44 @@ module Suspended = struct
6464
Effect.Deep.discontinue t.k ex
6565
end
6666

67+
(* Resume the next runnable fiber, if any. *)
68+
let rec wakeup run_q =
69+
match Run_queue.pop run_q with
70+
| Some f ->
71+
f ();
72+
wakeup run_q
73+
| None -> ()
74+
6775
(* The Javascript backend scheduler is implemented as an event listener.
6876
We don't need to worry about multiple domains. Here any time something
6977
asynchronously enqueues a task to our queue, it also sends a wakeup event to
7078
the event listener which will run the callback calling the scheduler. *)
7179
module Scheduler = struct
7280
type t = {
73-
scheduler : El.t;
7481
run_q : (unit -> unit) Run_queue.t;
75-
mutable listener : Ev.listener;
82+
mutable idle_callback : Jv.t option;
7683
}
7784

78-
let v ~schedule run_q =
79-
let open Brr_io in
80-
let scheduler = El.div [] in
81-
let listener =
82-
Brr.Ev.listen Message.Ev.message (fun _ev -> schedule run_q) (El.as_target scheduler)
83-
in
84-
{ scheduler; run_q; listener }
85+
let v run_q =
86+
let idle_callback = None in
87+
{ run_q; idle_callback }
8588

86-
let stop t = Brr.Ev.unlisten t.listener
89+
external _request_idle_callback : Jv.t -> Jv.t = "requestIdleCallbackShim"
90+
external _cancel_idle_callback : Jv.t -> unit = "cancelIdleCallbackShim"
91+
92+
let request_idle_callback cb =
93+
_request_idle_callback (Jv.callback ~arity:1 (fun _ -> cb ()))
8794

88-
(* A new message must be created for every call. *)
8995
let wakeup t =
90-
let open Brr_io in
91-
let args = [| Ev.create Message.Ev.message |> Ev.to_jv |] in
92-
Jv.call (El.to_jv t.scheduler) "dispatchEvent" args |> ignore
96+
(* No need to schedule a wakeup if the idle_callback is already set. *)
97+
if Option.is_some t.idle_callback then () else begin
98+
let idle_callback = request_idle_callback (fun () -> t.idle_callback <- None; wakeup t.run_q) in
99+
t.idle_callback <- Some idle_callback
100+
end
101+
102+
let stop t =
103+
Option.iter _cancel_idle_callback t.idle_callback;
104+
t.idle_callback <- None
93105

94106
let enqueue_thread t k v =
95107
Run_queue.push t.run_q (fun () -> Suspended.continue k v);
@@ -98,19 +110,15 @@ module Scheduler = struct
98110
let enqueue_failed_thread t k v =
99111
Run_queue.push t.run_q (fun () -> Suspended.discontinue k v);
100112
wakeup t
113+
114+
let enqueue_at_head t k v =
115+
Run_queue.push_head t.run_q (fun () -> Suspended.continue k v);
116+
wakeup t
101117
end
102118

103119
type _ Effect.t += Enter_unchecked : (Scheduler.t -> 'a Suspended.t -> unit) -> 'a Effect.t
104120
let enter_unchecked fn = Effect.perform (Enter_unchecked fn)
105121

106-
(* Resume the next runnable fiber, if any. *)
107-
let rec schedule run_q : unit =
108-
match Run_queue.pop run_q with
109-
| Some f ->
110-
f ();
111-
schedule run_q
112-
| None -> ()
113-
114122
module Timeout = struct
115123
let sleep ~ms =
116124
enter_unchecked @@ fun st k ->
@@ -147,10 +155,10 @@ let next_event : 'a Brr.Ev.type' -> Brr.Ev.target -> 'a Brr.Ev.t = fun typ targe
147155
(* Largely based on the Eio_mock.Backend event loop. *)
148156
let run main =
149157
let run_q = Run_queue.create () in
150-
let scheduler = Scheduler.v ~schedule run_q in
158+
let scheduler = Scheduler.v run_q in
151159
let rec fork ~new_fiber:fiber fn =
152160
Effect.Deep.match_with fn ()
153-
{ retc = (fun () -> Fiber_context.destroy fiber; schedule run_q);
161+
{ retc = (fun () -> Fiber_context.destroy fiber);
154162
exnc = (fun ex ->
155163
let bt = Printexc.get_raw_backtrace () in
156164
Fiber_context.destroy fiber;
@@ -159,18 +167,18 @@ let run main =
159167
effc = fun (type a) (e : a Effect.t) : ((a, unit) Effect.Deep.continuation -> unit) option ->
160168
match e with
161169
| Eio.Private.Effects.Suspend f -> Some (fun k ->
170+
let k = { Suspended.k; fiber } in
162171
f fiber (function
163-
| Ok v -> Run_queue.push run_q (fun () -> Effect.Deep.continue k v)
164-
| Error ex -> Run_queue.push run_q (fun () -> Effect.Deep.discontinue k ex)
165-
);
166-
schedule run_q
172+
| Ok v -> Scheduler.enqueue_thread scheduler k v
173+
| Error ex -> Scheduler.enqueue_failed_thread scheduler k ex
174+
)
167175
)
168176
| Enter_unchecked fn -> Some (fun k ->
169-
fn scheduler { Suspended.k; fiber };
170-
schedule run_q
177+
fn scheduler { Suspended.k; fiber }
171178
)
172179
| Eio.Private.Effects.Fork (new_fiber, f) -> Some (fun k ->
173-
Run_queue.push_head run_q (Effect.Deep.continue k);
180+
let k = { Suspended.k; fiber } in
181+
Scheduler.enqueue_at_head scheduler k ();
174182
fork ~new_fiber f
175183
)
176184
| Eio.Private.Effects.Get_context -> Some (fun k ->

lib_eio_js/browser/example/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
(alias runtest)
1111
(deps index.bc)
1212
(targets index.bc.js)
13-
(action (run %{bin:js_of_ocaml} -o %{targets} --enable=effects %{dep:index.bc})))
13+
(action (run %{bin:js_of_ocaml} -o %{targets} --enable=effects %{lib:eio_browser:runtime.js} %{dep:index.bc})))
1414

1515
(rule
1616
(alias runtest)

lib_eio_js/browser/runtime.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// A shim for safari: https://developer.chrome.com/blog/using-requestidlecallback/
2+
3+
// Provides: requestIdleCallbackShim
4+
function requestIdleCallbackShim (cb) {
5+
if (window.requestIdleCallback) {
6+
window.requestIdleCallback(cb)
7+
} else {
8+
var start = Date.now();
9+
globalThis.setTimeout(function () {
10+
cb({
11+
didTimeout: false,
12+
timeRemaining: function () {
13+
return Math.max(0, 50 - (Date.now() - start));
14+
}
15+
});
16+
}, 1);
17+
}
18+
}
19+
20+
// Provides: cancelIdleCallbackShim
21+
function cancelIdleCallbackShim (id) {
22+
if (window.cancelIdleCallback) {
23+
window.cancelIdleCallback(id);
24+
} else {
25+
globalThis.clearTimeout(id);
26+
}
27+
}

lib_eio_js/browser/stubs.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
#include <stdlib.h>
3+
#include <stdio.h>
4+
void requestIdleCallbackShim () { fprintf(stderr, "Unimplemented Javascript primitive requestIdleCallbackShim!\n"); exit(1); }
5+
void cancelIdleCallbackShim () { fprintf(stderr, "Unimplemented Javascript primitive cancelIdleCallbackShim!\n"); exit(1); }

lib_eio_js/browser/test/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
(alias runtest)
1414
(deps test.bc)
1515
(targets test.bc.js)
16-
(action (run %{bin:js_of_ocaml} -o %{targets} --enable=effects --setenv=ALCOTEST_COLOR=always +alcotest/runtime.js %{dep:test.bc})))
16+
(action (run %{bin:js_of_ocaml} -o %{targets} --enable=effects --setenv=ALCOTEST_COLOR=always %{lib:eio_browser:runtime.js} +alcotest/runtime.js %{dep:test.bc})))
1717

1818
(rule
1919
(alias runtest)

lib_eio_js/browser/test/test.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ module Browser_tests = struct
7676
let test_multiple_timeouts () =
7777
let lst = List.init 100 Fun.id in
7878
let v =
79-
Eio_browser.Timeout.sleep ~ms:1; lst
79+
Fiber.List.map (fun v -> Eio_browser.Timeout.sleep ~ms:100; v) lst
8080
in
8181
Alcotest.(check (list int)) "timeouts" lst v
8282

0 commit comments

Comments
 (0)