Skip to content

Commit f31abbc

Browse files
authored
feat: skip recursive frames (#5)
OCaml is functional and loves recursion, but these recursive frames show up in the callstack. Pyroscope has a default limit of ~1k frames before it starts lumping them together as "other". This PR makes it so we essentially remove any duplicated recursive frames, resulting in an easier to read trace, and more function calls being explicitly recorded. I think the con here is technically you won't be able to see say, "element # 10 to List.map takes a really long time", but I'd argue that's not really the point of continuous profiling, since we care about things en masse and how long functions overall take instead of specific instances. I also added a .ocamlformat ## Test plan run the example program, notice recursive calls to List.map on old pyro caml. run w/this PR and notice them all collapsed into 1.
2 parents 348f659 + 9328ae1 commit f31abbc

File tree

5 files changed

+67
-21
lines changed

5 files changed

+67
-21
lines changed

.ocamlformat

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
profile = conventional
2+
3+
# Preserve begin/end or parentheses to group expressions. Discussed November
4+
# 2024 with mixed opinions. Some prefer the consistency of always using
5+
# parentheses. Others find it easier to visually parse large blocks when
6+
# delimited by begin/end. For now we'll relax the constraints and let people
7+
# experiment. We can easily move back to enforcing parens only if we want to.
8+
exp-grouping = preserve
9+
10+
# Docstrings are supposed to be structured and parsable. We don't use them
11+
# properly, which makes ocamlformat 0.27.0 sad. Disable docstring parsing until
12+
# we get it together.
13+
parse-docstrings = false

dune-project

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
(source
88
(github semgrep/pyro-caml))
99

10-
; with ocaml linking exception
10+
; with ocaml linking exception?
1111
(license LGPL-2.1-only)
1212

1313
(authors "Austin Theriault <austin@cutedogs.org>")
@@ -17,7 +17,7 @@
1717
(package
1818
(name pyro-caml-instruments)
1919
(synopsis "Pyroscope + OCaml = Pyro Caml")
20-
(depends ocaml logs digestif)
20+
(depends ocaml logs digestif ppx_deriving)
2121
(tags ("profiling")))
2222

2323
(package

lib/Stack_trace.ml

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
type slot = Printexc.backtrace_slot
2121

2222
(* What's sent via runtime events. this HAS to be marshalable*)
23-
type raw_stack_trace = {slots: slot array; domain_id: int; thread_name: string}
23+
type raw_stack_trace = {
24+
slots : slot array;
25+
domain_id : int;
26+
thread_name : string;
27+
}
2428

2529
let raw_stack_trace_of_backtrace bt : raw_stack_trace =
2630
(* Use the domain as the ID since runtime event sampling happens per domain *)
@@ -30,7 +34,7 @@ let raw_stack_trace_of_backtrace bt : raw_stack_trace =
3034
let name = if Domain.is_main_domain () then "main" else string_of_int did in
3135
(* if there aren't any slots then not much we can do *)
3236
let slots = Option.value ~default:[||] Printexc.(backtrace_slots bt) in
33-
{slots; domain_id= did; thread_name= name}
37+
{ slots; domain_id = did; thread_name = name }
3438

3539
(*****************************************************************************)
3640
(* Stack frames *)
@@ -39,39 +43,65 @@ let raw_stack_trace_of_backtrace bt : raw_stack_trace =
3943
(* Inlined functions are filtered out in ocaml_intf always right now, but we
4044
probably want to give that as an option at some point *)
4145
(* coupling: ocaml_intf *)
42-
type frame = {name: string; filename: string; line: int; inlined: bool}
46+
type frame = {
47+
name : string;
48+
filename : string;
49+
line : int;
50+
inlined : bool; [@eq.skip]
51+
(* We really don't care about if a function is inlined for equality*)
52+
}
53+
[@@deriving eq]
4354

4455
let stack_frame_of_slot (slot : Printexc.backtrace_slot) : frame option =
4556
let loc = Printexc.Slot.location slot in
4657
let name = Printexc.Slot.name slot in
4758
let inlined = Printexc.Slot.is_inline slot in
4859
match (loc, name) with
4960
| Some loc, Some name ->
50-
Some {name; filename= loc.filename; line= loc.line_number; inlined}
51-
| None, Some name ->
52-
Some {name; filename= "<unknown>"; line= 0; inlined}
61+
Some { name; filename = loc.filename; line = loc.line_number; inlined }
62+
| None, Some name -> Some { name; filename = "<unknown>"; line = 0; inlined }
5363
| Some loc, None ->
5464
Some
55-
{ name= "<unknown>"
56-
; filename= loc.filename
57-
; line= loc.line_number
58-
; inlined }
59-
| None, None ->
60-
None
65+
{
66+
name = "<unknown>";
67+
filename = loc.filename;
68+
line = loc.line_number;
69+
inlined;
70+
}
71+
| None, None -> None
72+
73+
(* Looking ahead by up to 3 can be useful for recursive functions to make them
74+
much more legible. E.g. List.map can be very recursive, and pyroscope has a
75+
~1000 stack frame limit, so if we iterate through say, 10k items there's a
76+
good chance we may max out, which will cause any frames past the limit to be
77+
dropped and instead replaced with a single frame that says "other"*)
78+
let compress frames =
79+
let rec aux acc = function
80+
| [] -> List.rev acc
81+
| f1 :: f2 :: f3 :: (f4 :: f5 :: f6 :: _ as rest)
82+
when equal_frame f1 f4 && equal_frame f2 f5 && equal_frame f3 f6 ->
83+
aux acc rest
84+
| f1 :: f2 :: (f3 :: f4 :: _ as rest)
85+
when equal_frame f1 f3 && equal_frame f2 f4 ->
86+
aux acc rest
87+
| f1 :: (f2 :: _ as rest) when equal_frame f1 f2 -> aux acc rest
88+
| f :: rest -> aux (f :: acc) rest
89+
in
90+
aux [] frames
6191

6292
let stack_frames_of_slots slots =
63-
slots
64-
|> List.map (fun slot -> slot |> stack_frame_of_slot)
65-
|> List.filter_map Fun.id
93+
slots |> List.filter_map stack_frame_of_slot |> compress
6694

6795
(*****************************************************************************)
6896
(* Stack traces *)
6997
(*****************************************************************************)
7098
(* coupling: ocaml_intf *)
71-
type t = {frames: frame list; thread_id: int; thread_name: string}
99+
type t = { frames : frame list; thread_id : int; thread_name : string }
72100

73101
let t_of_raw_stack_trace raw_stack_trace =
74102
let frames = stack_frames_of_slots (Array.to_list raw_stack_trace.slots) in
75-
{ frames
76-
; thread_id= raw_stack_trace.domain_id
77-
; thread_name= raw_stack_trace.thread_name }
103+
{
104+
frames;
105+
thread_id = raw_stack_trace.domain_id;
106+
thread_name = raw_stack_trace.thread_name;
107+
}

lib/dune

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
(name pyro_caml_instruments)
33
(public_name pyro-caml-instruments)
44
(libraries runtime_events unix logs digestif)
5+
(preprocess
6+
(pps ppx_deriving.eq))
57
(instrumentation.backend
68
(ppx pyro-caml-ppx)))

pyro-caml-instruments.opam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ depends: [
1212
"ocaml"
1313
"logs"
1414
"digestif"
15+
"ppx_deriving"
1516
"odoc" {with-doc}
1617
]
1718
build: [

0 commit comments

Comments
 (0)