Skip to content

Commit 23101a6

Browse files
authored
More detailed exception backtraces (#6969)
Newer OCaml 4.x versions provide more details about a backtrace: * the location within a line (column), this is useful when you have a long `|>` pipeline for example * the name of the function (useful for a quick glance at what could be wrong) Previously `xapi-backtrace` used to parse the string formatted exceptions, and tried to extract file and line number information from it. Use the newer functions in `Printexc` to do this instead. String formatting needs to remain in the our `backtrace` module, because we want to consistently format backtraces from other languages too. This is an example of how the backtrace would look like after these changes: ``` Raised Db_exn.DBCache_NotFound("missing table", "Observer", "") 1/4 /opt/xensource/bin/xapi @ xrtmia-13-01 Raised at Xapi_database__Db_cache_types.TableSet.find in file "ocaml/database/db_cache_types.ml", line 298, characters 22-73 2/4 /opt/xensource/bin/xapi @ xrtmia-13-01 Called from Xapi_database__Db_cache_impl.read_refs in file "ocaml/database/db_cache_impl.ml", line 314, characters 12-70 3/4 /opt/xensource/bin/xapi @ xrtmia-13-01 Called from Db_actions.DB_Action.Observer.get_all in file "ocaml/xapi/db_actions.ml", line 23053, characters 40-69 4/4 /opt/xensource/bin/xapi @ xrtmia-13-01 Called from Xapi_observer_components.is_component_enabled.(fun) in file "ocaml/xapi/xapi_observer_components.ml", line 83, characters 26-56 ``` Also prevent long `List.map` sequences in a backtrace, by deduplicating adjacent identical entries in a backtrace. These changes also speed up calls to `Backtrace.is_important` ~10x, although performance isn't the main goal here (raising exceptions shouldn't be on the common path)
2 parents 890362f + 2cb3976 commit 23101a6

6 files changed

Lines changed: 122 additions & 47 deletions

File tree

ocaml/libs/backtrace/lib/backtrace.ml

Lines changed: 108 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,66 @@ module Mutex = struct
3030
Mutex.unlock lock ; r
3131
end
3232

33-
type frame = {process: string; filename: string; line: int} [@@deriving sexp]
33+
type frame = {
34+
process: string
35+
; filename: string
36+
; line: int
37+
; chars_start: int [@sexp.default 0]
38+
; chars_end: int [@sexp.default 0]
39+
; name: string option [@sexp.option]
40+
; is_inline: bool [@sexp.default false]
41+
; is_raise: bool [@sexp.default false]
42+
}
43+
[@@deriving sexp]
3444

3545
type t = frame list [@@deriving sexp]
3646

3747
let empty = []
3848

49+
(** [drop_wrapper str] drops anything before the last __.
50+
E.g `Dune_exe__Raiser` becomes `Raiser`,
51+
and `Xapi_database__Db_cache_impl` becomes `Db_cache_impl`.
52+
This makes backtraces easier to read, and the ambiguity introduced can be
53+
solved by looking at the filename and line number that is printed in the
54+
backtrace itself.
55+
*)
56+
let drop_wrapper str =
57+
str
58+
|> Astring.String.cut ~rev:true ~sep:"__"
59+
|> Option.fold ~none:str ~some:snd
60+
3961
let to_string_hum xs =
4062
let xs' = List.length xs in
4163
let results = Buffer.create 10 in
4264
let rec loop first_line i = function
4365
| [] ->
4466
Buffer.contents results
4567
| x :: xs ->
68+
Printf.bprintf results "%d/%d %s " i xs' x.process ;
4669
Buffer.add_string results
47-
(Printf.sprintf "%d/%d %s %s file %s, line %d" i xs' x.process
48-
( if first_line then
49-
"Raised at"
50-
else
51-
"Called from"
52-
)
53-
x.filename x.line
70+
( if first_line then
71+
"Raised at"
72+
else if x.is_raise then
73+
"Re-raised at"
74+
else
75+
"Called from"
5476
) ;
55-
Buffer.add_string results "\n" ;
77+
(* A standard OCaml stacktrace would look like this:
78+
Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
79+
Called from X.bar in file "x.ml" (inlined), line 2, characters 2-17
80+
*)
81+
Buffer.add_char results ' ' ;
82+
x.name
83+
|> Option.iter (fun name ->
84+
Buffer.add_string results (drop_wrapper name) ;
85+
Buffer.add_string results " in "
86+
) ;
87+
Printf.bprintf results "file %S" x.filename ;
88+
if x.is_inline then Buffer.add_string results " (inlined)" ;
89+
Printf.bprintf results ", line %d" x.line ;
90+
if x.chars_start > 0 then
91+
Printf.bprintf results ", characters %d-%d" x.chars_start x.chars_end ;
92+
Buffer.add_char results '\n' ;
5693
loop false (i + 1) xs
5794
in
5895
match xs with
@@ -74,31 +111,55 @@ type table = {
74111
be enough. *)
75112
let max_backtraces = 100
76113

77-
let frame_of_string process x =
78-
try
79-
begin match String.split_on_char '"' x with
80-
| [_; filename; rest] -> begin
81-
match String.split_on_char ',' rest with
82-
| [_; line_n; _] -> begin
83-
match String.split_on_char ' ' line_n with
84-
| _ :: _ :: n :: _ ->
85-
{process; filename; line= int_of_string n}
86-
| _ ->
87-
failwith (Printf.sprintf "Failed to parse line: [%s]" line_n)
88-
end
89-
| _ ->
90-
failwith (Printf.sprintf "Failed to parse fragment: [%s]" filename)
91-
end
92-
| _ ->
93-
failwith (Printf.sprintf "Failed to parse fragment: [%s]" x)
94-
end
95-
with e -> {process; filename= "(" ^ Printexc.to_string e ^ ")"; line= 0}
96-
97-
let get_backtrace_401 () =
98-
Printexc.get_backtrace ()
99-
|> String.split_on_char '\n'
100-
|> List.filter (fun x -> x <> "")
101-
|> List.map (frame_of_string !my_name)
114+
let frame_of_slot slot =
115+
let open Printexc in
116+
match Printexc.Slot.location slot with
117+
| None ->
118+
None
119+
| Some loc ->
120+
Some
121+
{
122+
process= !my_name
123+
; filename= loc.filename
124+
; line= loc.line_number
125+
; chars_start= loc.start_char
126+
; chars_end= loc.end_char
127+
; name= Slot.name slot
128+
; is_inline= Slot.is_inline slot
129+
; is_raise= Slot.is_raise slot
130+
}
131+
132+
let frame_eq a b =
133+
a.process == b.process
134+
&& a.line = b.line
135+
&& a.chars_start = b.chars_start
136+
&& a.chars_end = b.chars_end
137+
&& String.equal a.filename b.filename
138+
139+
let[@tail_mod_cons] rec dedup_to_list' last xs =
140+
match xs () with
141+
| Seq.Nil ->
142+
[]
143+
| Seq.Cons (x, xs) ->
144+
if frame_eq last x then
145+
dedup_to_list' x xs
146+
else
147+
x :: dedup_to_list' x xs
148+
149+
let dedup_to_list seq =
150+
match seq () with
151+
| Seq.Nil ->
152+
[]
153+
| Seq.Cons (x, xs) ->
154+
x :: dedup_to_list' x xs
155+
156+
let frames_of_slots slots =
157+
slots |> Array.to_seq |> Seq.filter_map frame_of_slot |> dedup_to_list
158+
159+
let get_backtrace_411 () =
160+
Printexc.get_raw_backtrace ()
161+
|> Printexc.backtrace_slots
162+
|> Option.fold ~none:[] ~some:frames_of_slots
102163

103164
let make () =
104165
let backtraces = Array.make max_backtraces [] in
@@ -117,7 +178,7 @@ let add t exn bt =
117178
)
118179

119180
let is_important t exn =
120-
let bt = get_backtrace_401 () in
181+
let bt = get_backtrace_411 () in
121182
(* Deliberately clear the backtrace buffer *)
122183
(try raise Not_found with Not_found -> ()) ;
123184
add t exn bt
@@ -291,5 +352,16 @@ module Interop = struct
291352
let of_json source_name txt =
292353
txt |> Jsonrpc.of_string |> error_of_rpc |> fun e ->
293354
List.combine e.files e.lines
294-
|> List.map (fun (filename, line) -> {process= source_name; filename; line})
355+
|> List.map (fun (filename, line) ->
356+
{
357+
process= source_name
358+
; filename
359+
; line
360+
; chars_start= 0
361+
; chars_end= 0
362+
; name= None
363+
; is_inline= false
364+
; is_raise= false
365+
}
366+
)
295367
end

ocaml/libs/backtrace/lib/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
(name backtrace)
33
(public_name xapi-log.backtrace)
44
(flags (:standard -w -39-32))
5-
(libraries
5+
(libraries astring
66
rpclib.core rpclib.json threads.posix)
77
(preprocess (pps ppx_deriving_rpc ppx_sexp_conv)))

ocaml/libs/backtrace/test/reraise.t

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
$ ./raiser.exe -reraise
77
Backtrace lab failed with exception Failure("bar")
88
Raised Failure("bar")
9-
1/4 raiser.exe Raised at file ocaml/libs/backtrace/test/raiser.ml, line 1
10-
2/4 raiser.exe Called from file ocaml/libs/backtrace/test/raiser.ml, line 4
11-
3/4 raiser.exe Called from file ocaml/libs/backtrace/lib/backtrace.ml, line 279
12-
4/4 raiser.exe Called from file ocaml/libs/backtrace/test/log.ml, line 59
9+
1/4 raiser.exe Raised at Raiser.foo in file "ocaml/libs/backtrace/test/raiser.ml", line 1, characters 20-41
10+
2/4 raiser.exe Called from Raiser.bar in file "ocaml/libs/backtrace/test/raiser.ml", line 4, characters 6-12
11+
3/4 raiser.exe Re-raised at Backtrace.reraise in file "ocaml/libs/backtrace/lib/backtrace.ml", line 340, characters 2-14
12+
4/4 raiser.exe Called from Log.with_thread_associated in file "ocaml/libs/backtrace/test/log.ml", line 59, characters 6-9
1313

1414

15+
1516
$ ./raiser.exe -v1-with-backtrace
1617
Backtrace lab failed with exception Failure("foo")
1718
Raised Failure("foo")
@@ -20,8 +21,9 @@
2021
$ ./raiser.exe -raise-again
2122
Backtrace lab failed with exception Failure("foo")
2223
Raised Failure("foo")
23-
1/4 raiser.exe Raised at file ocaml/libs/backtrace/test/raiser.ml, line 1
24-
2/4 raiser.exe Called from file ocaml/libs/backtrace/test/raiser.ml, line 6
25-
3/4 raiser.exe Called from file ocaml/libs/backtrace/test/raiser.ml, line 6
26-
4/4 raiser.exe Called from file ocaml/libs/backtrace/test/log.ml, line 59
24+
1/4 raiser.exe Raised at Raiser.foo in file "ocaml/libs/backtrace/test/raiser.ml", line 1, characters 20-41
25+
2/4 raiser.exe Called from Raiser.baz in file "ocaml/libs/backtrace/test/raiser.ml", line 6, characters 17-23
26+
3/4 raiser.exe Re-raised at Raiser.baz in file "ocaml/libs/backtrace/test/raiser.ml", line 6, characters 36-45
27+
4/4 raiser.exe Called from Log.with_thread_associated in file "ocaml/libs/backtrace/test/log.ml", line 59, characters 6-9
2728

29+

ocaml/libs/http-lib/test_client_server.t

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
1
1313
$ grep "backtrace" result -c
1414
11
15-
$ grep "Called from" result -c
15+
$ grep -E "Called from|Re-raised at" result -c
1616
8

ocaml/xcp-rrdd/bin/rrdd/rrdd_server.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ module Plugin = struct
696696
incr_skip_count uid plugin ;
697697
(* increase skip count *)
698698
let log e =
699+
Backtrace.is_important e ;
699700
info "Failed to process plugin metrics file: %s (%s)"
700701
(P.string_of_uid ~uid) (Printexc.to_string e) ;
701702
log_backtrace e

quality-gate.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ mli-files () {
4444
}
4545

4646
structural-equality () {
47-
N=8
47+
N=9
4848
EQ=$(git grep -r --count ' == ' -- '**/*.ml' ':!ocaml/sdk-gen/**/*.ml' | cut -d ':' -f 2 | paste -sd+ - | bc)
4949
if [ "$EQ" -eq "$N" ]; then
5050
echo "OK counted $EQ usages of ' == '"

0 commit comments

Comments
 (0)