Source file location_table.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
open! Core
open! Bonsai_web
open Memtrace_viewer_common

module Row = struct
  type t =
    { fragment : Data.Fragment.t
    ; display : Data.Location.t list
    ; allocations : Byte_units.Stable.V2.t
    }

  module Id = Data.Backtrace

  let compare (id1, t1) (id2, t2) =
    match Byte_units.compare t2.allocations t1.allocations with
    | 0 -> Data.Backtrace.compare id1 id2
    | c -> c
  ;;

  let id { fragment; _ } = Data.Fragment.backtrace fragment
end

module Table = struct
  module Row = Row

  module Col_id = struct
    module T = struct
      type t =
        | Location
        | Allocations
        | Percentage
      [@@deriving sexp, compare]
    end

    include T
    include Comparable.Make (T)
  end

  include Bonsai_simple_table.Make (Row) (Col_id)
end

let view_location extra loc ~call_sites =
  let call_sites = Data.Call_sites.for_location call_sites loc in
  Vdom.Node.span
    ~attrs:[ Vdom.Attr.title (Data.Location.full_name loc ~call_sites:(Some call_sites)) ]
    [ Vdom.Node.text extra; Location.format_dom loc ~call_sites:None ]
;;

let cell ?(attr = Vdom.Attr.empty) node = { Table.node; attrs = [ attr ] }
let attr_if cond attr = if cond then attr else Vdom.Attr.empty

let location_col ~call_sites ~standard_attrs =
  let open Vdom in
  let header = cell (Node.text "Location") in
  let render _ (row : Table.Row.t) =
    let node =
      match row.display with
      | [] -> Node.div ~attrs:[ Attr.class_ "last-loc" ] [ Node.text "(empty)" ]
      | first :: rest ->
        (match List.rev rest with
         | [] ->
           Node.div
             ~attrs:[ Attr.class_ "last-loc" ]
             [ view_location "" first ~call_sites ]
         | last :: _ ->
           Node.span
             [ Node.div
                 ~attrs:[ Attr.class_ "last-loc" ]
                 [ view_location "" last ~call_sites ]
             ; Node.div
                 ~attrs:[ Attr.class_ "first-loc" ]
                 [ view_location "..." first ~call_sites ]
             ])
    in
    let attr = standard_attrs ~fragment:row.fragment in
    cell node ~attr
  in
  let classes = [ "locations-column" ] in
  let group = None in
  Table.Column.create ~classes ~header ~render ~group ()
;;

let special_class ~fragment =
  attr_if (Data.Fragment.is_trivial fragment) (Vdom.Attr.class_ "loc-special")
;;

let allocations_col ~standard_attrs =
  let header = cell (Vdom.Node.text "Allocs") in
  let render _id ({ fragment; allocations; _ } : Table.Row.t) =
    let node = Vdom.Node.text (Byte_units.Short.to_string allocations) in
    let attr = Vdom.Attr.many [ special_class ~fragment; standard_attrs ~fragment ] in
    cell ~attr node
  in
  let classes = [ "allocations-column" ] in
  let group = None in
  Table.Column.create ~classes ~header ~render ~group ()
;;

let percentage_col ~total_allocations ~standard_attrs =
  let header = cell Vdom.Node.none in
  let render _id ({ fragment; allocations; _ } : Table.Row.t) =
    let percentage = Byte_units.(allocations // total_allocations) *. 100. in
    let node = Vdom.Node.textf "%.1f%%" percentage in
    let attr = Vdom.Attr.many [ special_class ~fragment; standard_attrs ~fragment ] in
    cell ~attr node
  in
  let classes = [ "percentage-column" ] in
  let group = None in
  Table.Column.create ~classes ~header ~render ~group ()
;;

let standard_attrs ~fragment ~focus ~set_focus ~on_click_row =
  (* Annoyingly, these have to be put on each column individually, because
     [Bonsai_simple_table] doesn't have support for putting attributes on rows *)
  let focus_class =
    attr_if
      (Data.Fragment.same fragment focus)
      (Vdom.Attr.class_ "location-table-fragment-in-focus")
  in
  let click_handler = Vdom.Attr.on_click (fun _ -> on_click_row fragment) in
  let double_click_handler = Vdom.Attr.on_double_click (fun _ -> set_focus fragment) in
  Vdom.Attr.many [ focus_class; click_handler; double_click_handler ]
;;

let cols ~total_allocations ~call_sites ~focus ~set_focus ~on_click_row =
  let standard_attrs ~fragment =
    standard_attrs ~fragment ~focus ~set_focus ~on_click_row
  in
  Table.Col_id.Map.of_alist_exn
    [ Location, location_col ~call_sites ~standard_attrs
    ; Allocations, allocations_col ~standard_attrs
    ; Percentage, percentage_col ~total_allocations ~standard_attrs
    ]
;;

let key_handler ~set_focus ~table_key_handler ~selected_fragment =
  let open Vdom_keyboard in
  let key = Keystroke.create' in
  match selected_fragment with
  | Some selected_fragment ->
    let set_focus_command : Keyboard_event_handler.Command.t =
      let keys = [ key Space; key Enter ] in
      let description = "Focus on selected" in
      let group = None in
      let handler _ =
        Effect.Many
          [ set_focus selected_fragment
          ; (* Don't scroll the table on Space *)
            Effect.Prevent_default
          ]
      in
      { keys; description; group; handler }
    in
    Keyboard_event_handler.add_command_exn table_key_handler set_focus_command
  | None ->
    let key_handler =
      Keyboard_event_handler.add_disabled_key_exn table_key_handler (key Space)
    in
    let key_handler =
      Keyboard_event_handler.add_disabled_key_exn key_handler (key Enter)
    in
    key_handler
;;

type t =
  { view : Vdom.Node.t
  ; key_handler : Vdom_keyboard.Keyboard_event_handler.t
  ; selection : (Data.Backtrace.t * Data.Fragment.t) option
  ; set_selection : Data.Fragment.t option -> unit Vdom.Effect.t
  ; move_selection : [ `Prev | `Next ] -> unit Vdom.Effect.t
  }

let component
  ~total_allocations
  ~call_sites
  ~rows
  ~presorted
  ~focus
  ~set_focus
  ~on_click_row
  =
  let open Bonsai.Let_syntax in
  let%sub input : Table.Input.t Bonsai.Computation.t =
    let%arr focus = focus
    and set_focus = set_focus
    and on_click_row = on_click_row
    and total_allocations = total_allocations
    and call_sites = call_sites
    and row_specs = rows
    and presorted = presorted in
    let row_specs = List.map ~f:(fun row -> Row.id row, row) row_specs in
    let row_specs =
      if presorted then row_specs else List.sort ~compare:Row.compare row_specs
    in
    let row_ids_in_order = `These (List.map ~f:fst row_specs) in
    let col_ids_in_order = [ Table.Col_id.Location; Allocations; Percentage ] in
    let rows = Data.Backtrace.Map.of_alist_exn row_specs in
    let cols = cols ~total_allocations ~call_sites ~focus ~set_focus ~on_click_row in
    let table_attrs = [ Vdom.Attr.class_ "location-table" ] in
    Table.Input.create ~rows ~cols ~row_ids_in_order ~col_ids_in_order ~table_attrs ()
  in
  let%sub table = Table.bonsai input in
  return
    (let%map { Table.Result.view
             ; view_for_testing = _
             ; key_handler = table_key_handler
             ; focus_row
             ; inject
             }
       =
       table
     and set_focus = set_focus in
     let selection =
       Option.map focus_row ~f:(fun (backtrace, { fragment; _ }) -> backtrace, fragment)
     in
     let set_selection selection =
       inject (Set_focus_row (Option.map ~f:Data.Fragment.backtrace selection))
     in
     let move_selection dir = inject (Move_focus dir) in
     let key_handler =
       let selected_fragment = Option.map selection ~f:(fun (_, fragment) -> fragment) in
       key_handler ~table_key_handler ~set_focus ~selected_fragment
     in
     { view; key_handler; selection; set_selection; move_selection })
;;