Source file profile.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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
open! Core
module Graph_info = Bonsai.Private.Graph_info
module Interaction = Bonsai_bench_scenario.Interaction

module Id = struct
  let instance = String.Table.create ()

  let of_node_path node_path =
    let key = Bonsai.Private.Node_path.to_string node_path in
    match Hashtbl.find instance key with
    | Some id -> id
    | None ->
      let id = Hashtbl.length instance in
      Hashtbl.set instance ~key ~data:id;
      id
  ;;
end

let stop_timer timer =
  match Javascript_profiling.Timer.(stop timer |> duration) with
  | Ok duration -> duration
  | Backgrounding_changed_unreliable _duration ->
    failwith "BUG: Backgrounding should not have changed while benchmarking."
;;

module Measurement = struct
  module Kind = struct
    module T = struct
      type t =
        | Graph_application
        | Preprocess
        | Gather
        | Run_eval_fun
        | First_stabilization
        | Incremental_recompute of string
        | Stabilize_for_clock
        | Apply_actions
        | Stabilize_for_action
        | Stabilize_after_all_apply_actions
      [@@deriving sexp, equal, compare]

      let is_incremental_recompute = function
        | Incremental_recompute _ -> true
        | _ -> false
      ;;

      let is_stabilize = function
        | First_stabilization
        | Stabilize_for_clock
        | Stabilize_for_action
        | Stabilize_after_all_apply_actions -> true
        | _ -> false
      ;;
    end

    include T
    include Comparable.Make_plain (T)

    (* We don't record while printing results, so allocating strings in here should be
       fine. *)
    let to_string = function
      | Incremental_recompute label -> label
      | Graph_application -> "[startup] Graph Application"
      | Preprocess -> "[startup] Preprocess"
      | Gather -> "[startup] Gather"
      | Run_eval_fun -> "[startup] Run Eval Fun"
      | First_stabilization -> "[startup] First Stabilization"
      | Stabilize_for_clock -> "[runtime] Stabilize For Clock"
      | Apply_actions -> "[runtime] Apply Actions"
      | Stabilize_for_action -> "[runtime] Stabilize For Action"
      | Stabilize_after_all_apply_actions -> "[runtime] Stabilize After All Apply Actions"
    ;;
  end

  type t =
    { kind : Kind.t
    ; duration : Time_ns.Span.t
    ; id : int option
    }
  [@@deriving sexp]

  let create kind ~duration = { kind; duration; id = None }

  let annotate
    ~(source_locations : Graph_info.Node_info.t Bonsai.Private.Node_path.Map.t)
    measurement
    =
    match measurement.kind with
    | Incremental_recompute label ->
      let with_source_position =
        let%bind.Option node_path =
          Bonsai.Private.Instrumentation.extract_node_path_from_entry_label label
        in
        let%bind.Option { node_type; here } = Map.find source_locations node_path in
        let%map.Option here in
        { measurement with
          kind =
            Incremental_recompute [%string "%{node_type} (%{here#Source_code_position})"]
        ; id = Some (Id.of_node_path node_path)
        }
      in
      Option.value with_source_position ~default:measurement
    | Graph_application
    | Preprocess
    | Gather
    | Run_eval_fun
    | First_stabilization
    | Stabilize_for_clock
    | Apply_actions
    | Stabilize_for_action
    | Stabilize_after_all_apply_actions -> measurement
  ;;
end

module Accumulated_measurement = struct
  type t =
    { kind : Measurement.Kind.t
    ; total_duration : Time_ns.Span.t
    ; count : int
    ; id : int option
    }
  [@@deriving sexp]

  let compare
    { kind; total_duration; _ }
    { kind = kind'; total_duration = total_duration'; _ }
    =
    match kind, kind' with
    | Incremental_recompute _, Incremental_recompute _ ->
      Time_ns.Span.descending total_duration total_duration'
    | _, _ -> Measurement.Kind.compare kind kind'
  ;;

  let of_measurement { Measurement.kind; duration; id } =
    { kind; count = 1; total_duration = duration; id }
  ;;

  let add { kind; total_duration; count; id } ~measurement =
    assert (Measurement.Kind.equal kind measurement.Measurement.kind);
    { kind
    ; count = count + 1
    ; total_duration = Time_ns.Span.(total_duration + measurement.duration)
    ; id
    }
  ;;
end

let spans_pct a b =
  Percent.Always_percentage.to_string
    (Percent.of_percentage (Time_ns.Span.(to_ns a /. to_ns b) *. 100.))
;;

let create_summary_table ~total_time ~let_arr_time ~stabilize_time =
  let other_time = Time_ns.Span.(total_time - let_arr_time) in
  let open Ascii_table_kernel in
  to_string_noattr
    [ Column.create "Statistic" fst; Column.create "Value" snd ]
    ~limit_width_to:Int.max_value
    ~bars:`Unicode
    [ "Total time", Time_ns.Span.to_string_hum total_time
    ; "Stabilize time", Time_ns.Span.to_string_hum stabilize_time
    ; "let%arr time", Time_ns.Span.to_string_hum let_arr_time
    ; "let%arr / stabilize (%)", spans_pct let_arr_time stabilize_time
    ; "Non let%arr time", Time_ns.Span.to_string_hum other_time
    ; "Non let%arr time (%)", spans_pct other_time total_time
    ]
;;

let create_snapshot_table data ~total_time ~let_arr_time =
  let open Ascii_table_kernel in
  let columns =
    [ Column.create "Id" (fun { Accumulated_measurement.id; _ } ->
        match id with
        | Some int -> Int.to_string int
        | None -> "N/A")
    ; Column.create "Name" (fun { Accumulated_measurement.kind; _ } ->
        Measurement.Kind.to_string kind)
    ; Column.create "Times fired" (fun { Accumulated_measurement.count; _ } ->
        Int.to_string count)
    ; Column.create "Total time" (fun { Accumulated_measurement.total_duration; _ } ->
        Time_ns.Span.to_string_hum total_duration)
    ; Column.create
        "% of let%arr time"
        (fun { Accumulated_measurement.total_duration; kind; _ } ->
           match Measurement.Kind.is_incremental_recompute kind with
           | true -> spans_pct total_duration let_arr_time
           | false -> "")
    ; Column.create
        "% of total time"
        (fun { Accumulated_measurement.total_duration; _ } ->
           spans_pct total_duration total_time)
    ]
  in
  to_string_noattr columns data ~limit_width_to:Int.max_value ~bars:`Unicode
;;

let print_statistics ~duration data =
  let sorted_measurements =
    List.sort (Map.data data) ~compare:Accumulated_measurement.compare
  in
  let let_arr_time, stabilize_time =
    List.fold
      ~init:(Time_ns.Span.zero, Time_ns.Span.zero)
      sorted_measurements
      ~f:(fun (let_arr_acc, stabilize_acc) { kind; total_duration; _ } ->
        let add_if ~f base =
          if f kind then Time_ns.Span.(base + total_duration) else base
        in
        ( add_if ~f:Measurement.Kind.is_incremental_recompute let_arr_acc
        , add_if ~f:Measurement.Kind.is_stabilize stabilize_acc ))
  in
  print_endline "Summary:";
  print_endline (create_summary_table ~total_time:duration ~let_arr_time ~stabilize_time);
  print_endline "Details:";
  print_endline
    (create_snapshot_table sorted_measurements ~total_time:duration ~let_arr_time)
;;

let accumulate_measurements ~source_locations measurements =
  let with_ids, without_ids =
    List.map measurements ~f:(Measurement.annotate ~source_locations)
    |> List.fold
         ~init:(Int.Map.empty, Measurement.Kind.Map.empty)
         ~f:(fun (with_ids, without_ids) measurement ->
           let accumulate_measurements = function
             | None -> Accumulated_measurement.of_measurement measurement
             | Some accumulated -> Accumulated_measurement.add accumulated ~measurement
           in
           match measurement.id with
           | None ->
             with_ids, Map.update without_ids measurement.kind ~f:accumulate_measurements
           | Some id -> Map.update with_ids id ~f:accumulate_measurements, without_ids)
  in
  (* Assign measurements without IDs (e.g. startup) a new ID. *)
  Map.fold without_ids ~init:with_ids ~f:(fun ~key:_ ~data:measurement acc ->
    let id =
      match Map.max_elt acc with
      (* This could happen if the user never [let%sub]s. It's not very realistic for a
         practical app, but totally possible to write. *)
      | None -> 0
      | Some (id, _) -> id + 1
    in
    let measurement = { measurement with id = Some id } in
    Map.set acc ~key:id ~data:measurement)
;;

let take_profile_snapshot ~duration ~name graph_info recorded_measurements =
  match !recorded_measurements with
  | [] ->
    print_endline [%string "Not printing profile of %{name} because nothing happened"]
  | _ ->
    print_endline [%string "Bonsai_bench Profile: %{name}"];
    let source_locations =
      Graph_info.pull_source_locations_from_nearest_parent graph_info
    in
    print_statistics
      ~duration
      (accumulate_measurements ~source_locations !recorded_measurements);
    recorded_measurements := []
;;

let profile' ~time_source ~get_inject ~interaction ~component =
  let graph_info = ref Graph_info.empty in
  let recorded_measurements = ref [] in
  let store_entry entry = recorded_measurements := entry :: !recorded_measurements in
  let timer_since_last_snapshot = ref None in
  let handle_profile name =
    match !timer_since_last_snapshot with
    | None ->
      raise_s
        [%message
          "Bonsai_bench.profile BUG! No interactions should have been run prior to \
           startup."
            ([%here] : Source_code_position.t)]
    | Some timer ->
      let duration = stop_timer timer in
      take_profile_snapshot ~duration ~name !graph_info recorded_measurements;
      timer_since_last_snapshot := Some (Javascript_profiling.Timer.start ())
  in
  let runner =
    Runner.initialize
      ~filter_profiles:false
      ~driver_instrumentation:
        { instrument_for_computation_watcher =
            Ui_incr.return Bonsai.Private.Instrumentation.Watching.Not_watching
        ; instrument_for_profiling =
            Ui_incr.return Bonsai.Private.Instrumentation.Profiling.Profiling
        ; computation_watcher_queue =
            Queue.create ( (* We don't use the computation watcher. *) )
        ; set_latest_graph_info = (fun gi -> graph_info := gi)
        ; start_timer = (fun evt -> evt, Javascript_profiling.Timer.start ())
        ; stop_timer =
            (fun (evt, timer) ->
              let duration = stop_timer timer in
              let measurement_kind =
                match evt with
                | Profiling_entry s -> Measurement.Kind.Incremental_recompute s
                | Graph_application -> Graph_application
                | Preprocess -> Preprocess
                | Gather -> Gather
                | Run_eval_fun -> Run_eval_fun
                | First_stabilization -> First_stabilization
                | Stabilize_for_clock -> Stabilize_for_clock
                | Apply_actions -> Apply_actions
                | Stabilize_for_action -> Stabilize_for_action
                | Stabilize_after_all_apply_actions -> Stabilize_after_all_apply_actions
              in
              Measurement.create measurement_kind ~duration |> store_entry)
        }
      ~wrap_driver_creation:
        { f =
            (fun create_driver ->
              let timer = Javascript_profiling.Timer.start () in
              let driver = create_driver () in
              let duration = stop_timer timer in
              take_profile_snapshot
                ~duration
                ~name:"startup"
                !graph_info
                recorded_measurements;
              driver)
        }
      ~time_source
      ~component
      ~get_inject
      ~interaction
  in
  if not (List.is_empty !recorded_measurements)
  then
    raise_s
      [%message
        "Bonsai_bench.profile BUG: there should be no profiling entries between \
         profiling creation and running interactions"
          ([%here] : Source_code_position.t)];
  timer_since_last_snapshot := Some (Javascript_profiling.Timer.start ());
  Runner.run_interactions runner ~handle_profile;
  Runner.invalidate_observers runner
;;

let profile = function
  | Config.Startup { time_source; component; name } ->
    print_endline [%string "Running Bonsai_bench startup profile of %{name}"];
    profile'
      ~time_source
      ~get_inject:Config.startup_get_inject
      ~interaction:(Interaction.many [])
      ~component
  | Interactions { time_source; component; get_inject; interaction; name } ->
    print_endline [%string "Running Bonsai_bench profile of %{name}"];
    profile'
      ~time_source
      ~get_inject
      ~interaction:
        (Interaction.many
           [ interaction; Interaction.recompute; Interaction.profile ~name:"end of run" ])
      ~component
;;