package bonsai

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file forward_performance_entries.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
open Js_of_ocaml

(* This module has been upstreamed into js_of_ocaml, so we should remove it
   once the new compiler gets released to opam. *)
module PerformanceObserver : sig
  (* Js_of_ocaml library
   * http://www.ocsigen.org/js_of_ocaml/
   * Copyright (C) 2021 Philip White
   *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, with linking exception;
   * either version 2.1 of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  *)

  (** PerformanceObserver API

      A code example:
      {[
        if (PerformanceObserver.is_supported()) then
          let entry_types = [ "measure" ] in
          let f entries observer =
            let entries = entries##getEntries in
            Firebug.console##debug entries ;
            Firebug.console##debug observer
          in
          PerformanceObserver.observe ~entry_types ~f
            ()
      ]}

      @see <https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver> for API documentation.
  *)

  class type performanceObserverInit =
    object
      method entryTypes : Js.js_string Js.t Js.js_array Js.t Js.writeonly_prop
    end

  class type performanceEntry =
    object
      method name : Js.js_string Js.t Js.readonly_prop
      method entryType : Js.js_string Js.t Js.readonly_prop
      method startTime : float Js.readonly_prop
      method duration : float Js.readonly_prop
    end

  class type performanceObserverEntryList =
    object
      method getEntries : performanceEntry Js.t Js.js_array Js.t Js.meth
    end

  class type performanceObserver =
    object
      method observe : performanceObserverInit Js.t -> unit Js.meth
      method disconnect : unit Js.meth
      method takeRecords : performanceEntry Js.t Js.js_array Js.t Js.meth
    end

  val observe
    :  entry_types:string list
    -> f:(performanceObserverEntryList Js.t -> performanceObserver Js.t -> unit)
    -> performanceObserver Js.t
end = struct
  open Js_of_ocaml

  class type performanceObserverInit =
    object
      method entryTypes : Js.js_string Js.t Js.js_array Js.t Js.writeonly_prop
    end

  class type performanceEntry =
    object
      method name : Js.js_string Js.t Js.readonly_prop
      method entryType : Js.js_string Js.t Js.readonly_prop
      method startTime : float Js.readonly_prop
      method duration : float Js.readonly_prop
    end

  class type performanceObserverEntryList =
    object
      method getEntries : performanceEntry Js.t Js.js_array Js.t Js.meth
    end

  class type performanceObserver =
    object
      method observe : performanceObserverInit Js.t -> unit Js.meth
      method disconnect : unit Js.meth
      method takeRecords : performanceEntry Js.t Js.js_array Js.t Js.meth
    end

  let performanceObserver = Js.Unsafe.global##._PerformanceObserver

  let performanceObserver
    : ((performanceObserverEntryList Js.t -> performanceObserver Js.t -> unit)
         Js.callback
       -> performanceObserver Js.t)
        Js.constr
    =
    performanceObserver
  ;;

  let observe ~entry_types ~f =
    let entry_types = entry_types |> List.map Js.string |> Array.of_list |> Js.array in
    let performance_observer_init : performanceObserverInit Js.t = Js.Unsafe.obj [||] in
    let () = performance_observer_init##.entryTypes := entry_types in
    let obs = new%js performanceObserver (Js.wrap_callback f) in
    let () = obs##observe performance_observer_init in
    obs
  ;;
end

open! Core
open Bonsai.Private
open Bonsai_protocol

type ('model, 'static_action, 'dynamic_action, 'result) t =
  { instrumented_computation :
      ('model, 'static_action, 'dynamic_action, 'result) Bonsai.Private.Computation.t
  ; shutdown : unit -> unit
  }

module Worker : sig
  (** Represents a web worker that you can send messages to. This type handles
      annoying details such as making sure that the web worker is ready to
      start receiving messages, serializing the messages, and batching several
      of the messages together.  *)
  type 'a t

  (** Loads a web worker from the specified URL. [on_message] is called every
      time the web worker sends a message to the main thread. *)
  val create
    :  url:string
    -> on_message:(string -> unit)
    -> bin_writer_t:'a Bin_prot.Type_class.writer
    -> 'a t

  (** Queues a message to be sent at the next call to [flush]. *)
  val send_message : 'a t -> 'a -> unit

  (** Sends all the queued messages to the worker as a single message *)
  val flush : _ t -> unit

  val set_error_handler : _ t -> f:(Worker.errorEvent Js.t -> unit) -> unit
  val shutdown : _ t -> unit
end = struct
  (* The [acknowledged] field keeps track of whether the worker has sent back a
     message, which means that it is ready to receive messages. *)
  type 'a t =
    { mutable acknowledged : bool
    ; mutable buffer : 'a list
    ; worker : (Js.js_string Js.t, Js.js_string Js.t) Worker.worker Js.t
    ; bin_writer_t : 'a Bin_prot.Type_class.writer
    }

  let create ~url ~on_message ~bin_writer_t =
    (* We use a [blob] to circumvent the same-origin policy for web workers.
       Note that we aren't able to break through the browser's defenses
       totally, since the server must still configure its CSP to allow web
       workers from blobs. *)
    let worker =
      let blob =
        File.blob_from_string
          ~contentType:"application/javascript"
          [%string "importScripts('%{url}')"]
      in
      let blob_url = Dom_html.window##._URL##createObjectURL blob in
      Worker.create (Js.to_string blob_url)
    in
    let result = { worker; acknowledged = false; buffer = []; bin_writer_t } in
    worker##.onmessage
    := Dom.handler (fun (message : Js.js_string Js.t Worker.messageEvent Js.t) ->
      result.acknowledged <- true;
      on_message (Js.to_string message##.data);
      Js._false);
    result
  ;;

  let set_error_handler t ~f =
    t.worker##.onerror
    := Dom.handler (fun error_message ->
      f error_message;
      Js._false)
  ;;

  let send_message t message = t.buffer <- message :: t.buffer

  let flush t =
    if t.acknowledged
    then (
      let js_string =
        Js.bytestring
          (Bin_prot.Writer.to_string (List.bin_writer_t t.bin_writer_t) t.buffer)
      in
      t.worker##postMessage js_string;
      t.buffer <- [])
    else ()
  ;;

  let shutdown t =
    t.buffer <- [];
    t.worker##terminate
  ;;
end

let iter_entries performance_observer_entry_list ~f =
  performance_observer_entry_list##getEntries
  |> Js.to_array
  |> Array.iter ~f:(fun entry ->
    let label =
      let label = entry##.name |> Js.to_string in
      match Instrumentation.extract_node_path_from_entry_label label with
      | None -> `Other label
      | Some node_id -> `Bonsai node_id
    in
    let entry_type = entry##.entryType |> Js.to_bytestring in
    let start_time = entry##.startTime |> Js.to_float in
    let duration = entry##.duration |> Js.to_float in
    f { Entry.label; entry_type; start_time; duration })
;;

let instrument ~host ~port ~worker_name component =
  let worker =
    (* Once the worker sends an acknowledgement message, we can send the graph
       info. It's possible that the worker has already received a info message,
       but we're sending one now, just to be sure.

       The reason we need to do it this way is that we have no way of knowing
       when the web worker has set up its [onmessage] callback and is ready to
       receive message. Thus, we wait until the worker notifies us explicitly
       that it is ready to receive messages.

       This onmessage callback is also a convenient place to receive the UUID
       that identifies this profiling session. Since web workers cannot open
       new windows, we must open the window from the main page. To keep the URL
       of the server decoupled from this logic, we just receive the URL from
       the worker. *)
    Worker.create
      ~url:[%string "https://%{host}:%{port#Int}/%{worker_name}"]
      ~on_message:(fun url ->
        Dom_html.window##open_
          (Js.string url)
          (Js.string "bonsai-bug")
          (Js.Opt.return (Js.string "noopener"))
        |> (ignore : Dom_html.window Js.t Js.opt -> unit))
      ~bin_writer_t:Message.bin_writer_t
  in
  let performance_observer =
    let f new_entries observer =
      observer##takeRecords
      |> (ignore : PerformanceObserver.performanceEntry Js.t Js.js_array Js.t -> unit);
      iter_entries new_entries ~f:(fun entry ->
        Worker.send_message worker (Performance_measure entry))
    in
    PerformanceObserver.observe ~entry_types:[ "measure" ] ~f
  in
  let graph_info_dirty = ref false in
  let graph_info = ref Graph_info.empty in
  let component =
    Bonsai.Private.Graph_info.iter_graph_updates component ~on_update:(fun gi ->
      (* Instead of sending a message every time the graph changes, we maintain
         the current graph_info and mark it as dirty, so that the loop at the
         bottom of this function can send only one single message per flush. *)
      graph_info := gi;
      graph_info_dirty := true)
  in
  let instrumented_computation =
    Instrumentation.instrument_computation
      component
      ~start_timer:(fun s -> Javascript_profiling.Manual.mark (s ^ "before"))
      ~stop_timer:(fun s ->
        let before = s ^ "before" in
        let after = s ^ "after" in
        Javascript_profiling.Manual.mark after;
        Javascript_profiling.Manual.measure ~name:s ~start:before ~end_:after)
  in
  let stop_ivar = Async_kernel.Ivar.create () in
  let stop = Async_kernel.Ivar.read stop_ivar in
  Async_kernel.every ~stop (Time_ns.Span.of_sec 0.5) (fun () ->
    if !graph_info_dirty
    then (
      graph_info_dirty := false;
      Worker.send_message worker (Message.Graph_info !graph_info));
    Worker.flush worker;
    Javascript_profiling.clear_marks ();
    Javascript_profiling.clear_measures ());
  let shutdown () =
    Async_kernel.Ivar.fill_if_empty stop_ivar ();
    performance_observer##disconnect;
    Javascript_profiling.clear_marks ();
    Javascript_profiling.clear_measures ();
    Worker.shutdown worker
  in
  let shutdown () =
    match Or_error.try_with shutdown with
    | Ok () -> ()
    | Error e -> eprint_s [%sexp (e : Error.t)]
  in
  Worker.set_error_handler worker ~f:(fun message ->
    Firebug.console##warn message;
    shutdown ());
  { instrumented_computation; shutdown }
;;
OCaml

Innovation. Community. Security.