Skip to content

Commit fc0017f

Browse files
authored
Log a backtrace on entrance to type inference (#58124)
1 parent 965658a commit fc0017f

File tree

7 files changed

+117
-1
lines changed

7 files changed

+117
-1
lines changed

base/stacktraces.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,4 @@ function from(frame::StackFrame, m::Module)
375375
return parentmodule(frame) === m
376376
end
377377

378-
end
378+
end # module StackTraces

doc/src/devdocs/diagnostics.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Diagnostics used by the package ecosystem
2+
3+
This page documents "hooks" embedded in Julia that are primarily used by
4+
external tools. Many of these tools are designed to perform analyses that are
5+
too complicated to be made part of Julia proper.
6+
7+
## SnoopCompile
8+
9+
SnoopCompile "snoops" on Julia's compiler to extract information for analysis
10+
about invalidations and type-inference. There are a few internals it uses for
11+
different purposes:
12+
13+
- recording invalidations: `Base.StaticData.debug_method_invalidation` and
14+
`ccall(:jl_debug_method_invalidation, ...)`: these record different modes of
15+
invalidation. Users of SnoopCompile will transiently turn these on when, e.g.,
16+
loading packages. Each produces a standard log format; messing with the log
17+
format might require a complementary pull request to SnoopCompile.
18+
SnoopCompile will process these logs and generate trees of invalidated
19+
CodeInstances that are attributable to specific changes in the method tables
20+
or bindings.
21+
- observing inference: `ccall(:jl_set_newly_inferred, ...)` and
22+
`ccall(:jl_set_inference_entrance_backtraces, ...)`: these are used to
23+
understand how inference gets triggered. The main purpose is to allow
24+
performance diagnostics to understand sources of TTFX. The second of these
25+
`ccall`s records a backtrace on every entrance to type-inference, so that
26+
SnoopCompile can determine the caller of a dynamically-dispatched call. This
27+
is needed to attribute "cause" for new type inference.
28+
29+
The `jl_set_inference_entrance_backtraces` function accepts an array where
30+
inference entrance events will be recorded. Each inference event stores two
31+
consecutive array elements: first the `CodeInstance` object, then the
32+
backtrace representation. So for N inference events, the array will contain 2N
33+
elements arranged as: `[ci₁, bt₁, ci₂, bt₂, ..., ciₙ, btₙ]`.
34+
35+
Note that the backtrace elements `btᵢ` contain raw backtrace data that
36+
typically needs to be processed using `stacktrace(Base._reformat_bt(btᵢ...))`.
37+
to convert them into a usable stack trace format for analysis.

src/gf.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,14 @@ jl_code_instance_t *jl_type_infer(jl_method_instance_t *mi, size_t world, uint8_
492492
if (ci && !jl_is_code_instance(ci)) {
493493
ci = NULL;
494494
}
495+
496+
// Record inference entrance backtrace if enabled
497+
if (ci) {
498+
JL_GC_PUSH1(&ci);
499+
jl_push_inference_entrance_backtraces((jl_value_t*)ci);
500+
JL_GC_POP();
501+
}
502+
495503
JL_GC_POP();
496504
#endif
497505

src/julia.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2276,6 +2276,8 @@ JL_DLLEXPORT jl_value_t *jl_object_top_module(jl_value_t* v) JL_NOTSAFEPOINT;
22762276

22772277
JL_DLLEXPORT void jl_set_newly_inferred(jl_value_t *newly_inferred);
22782278
JL_DLLEXPORT void jl_push_newly_inferred(jl_value_t *ci);
2279+
JL_DLLEXPORT void jl_set_inference_entrance_backtraces(jl_value_t *inference_entrance_backtraces);
2280+
JL_DLLEXPORT void jl_push_inference_entrance_backtraces(jl_value_t *ci);
22792281
JL_DLLEXPORT void jl_write_compiler_output(void);
22802282

22812283
// parsing

src/julia_internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,7 @@ size_t rec_backtrace_ctx(jl_bt_element_t *bt_data, size_t maxsize, bt_context_t
15061506
size_t rec_backtrace_ctx_dwarf(jl_bt_element_t *bt_data, size_t maxsize, bt_context_t *ctx, jl_gcframe_t *pgcstack) JL_NOTSAFEPOINT;
15071507
#endif
15081508
JL_DLLEXPORT jl_value_t *jl_get_backtrace(void);
1509+
JL_DLLEXPORT jl_value_t *jl_backtrace_from_here(int returnsp, int skip);
15091510
void jl_critical_error(int sig, int si_code, bt_context_t *context, jl_task_t *ct);
15101511
JL_DLLEXPORT void jl_raise_debugger(void) JL_NOTSAFEPOINT;
15111512
JL_DLLEXPORT void jl_gdblookup(void* ip) JL_NOTSAFEPOINT;

src/staticdata_utils.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,38 @@ JL_DLLEXPORT void jl_push_newly_inferred(jl_value_t* ci)
131131
JL_UNLOCK(&newly_inferred_mutex);
132132
}
133133

134+
135+
static jl_array_t *inference_entrance_backtraces JL_GLOBALLY_ROOTED /*FIXME*/ = NULL;
136+
// Mutex for inference_entrance_backtraces
137+
jl_mutex_t inference_entrance_backtraces_mutex;
138+
139+
// Register array of inference entrance backtraces
140+
JL_DLLEXPORT void jl_set_inference_entrance_backtraces(jl_value_t* _inference_entrance_backtraces)
141+
{
142+
assert(_inference_entrance_backtraces == NULL || _inference_entrance_backtraces == jl_nothing || jl_is_array(_inference_entrance_backtraces));
143+
if (_inference_entrance_backtraces == jl_nothing)
144+
_inference_entrance_backtraces = NULL;
145+
JL_LOCK(&inference_entrance_backtraces_mutex);
146+
inference_entrance_backtraces = (jl_array_t*) _inference_entrance_backtraces;
147+
JL_UNLOCK(&inference_entrance_backtraces_mutex);
148+
}
149+
150+
151+
JL_DLLEXPORT void jl_push_inference_entrance_backtraces(jl_value_t* ci)
152+
{
153+
JL_LOCK(&inference_entrance_backtraces_mutex);
154+
if (inference_entrance_backtraces == NULL) {
155+
JL_UNLOCK(&inference_entrance_backtraces_mutex);
156+
return;
157+
}
158+
jl_value_t* backtrace = jl_backtrace_from_here(0, 1);
159+
size_t end = jl_array_nrows(inference_entrance_backtraces);
160+
jl_array_grow_end(inference_entrance_backtraces, 2);
161+
jl_array_ptr_set(inference_entrance_backtraces, end, ci);
162+
jl_array_ptr_set(inference_entrance_backtraces, end + 1, backtrace);
163+
JL_UNLOCK(&inference_entrance_backtraces_mutex);
164+
}
165+
134166
// compute whether a type references something internal to worklist
135167
// and thus could not have existed before deserialize
136168
// and thus does not need delayed unique-ing

test/stacktraces.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,39 @@ end
262262
@testset "Base.StackTraces docstrings" begin
263263
@test isempty(Docs.undocumented_names(StackTraces))
264264
end
265+
266+
267+
@testset "Dispatch backtraces" begin
268+
# Check that it's possible to capture a backtrace upon entrance to inference
269+
# This test ensures that SnoopCompile will continue working
270+
# See in particular SnoopCompile/SnoopCompileCore/src/snoop_inference.jl
271+
# and the "diagnostics" devdoc.
272+
@noinline callee(x::Int) = sin(x)
273+
caller(x) = invokelatest(callee, x)
274+
275+
@test sin(0) == 0 # force compilation of sin(::Int)
276+
dispatch_backtraces = []
277+
ccall(:jl_set_inference_entrance_backtraces, Cvoid, (Any,), dispatch_backtraces)
278+
caller(3)
279+
ccall(:jl_set_inference_entrance_backtraces, Cvoid, (Any,), nothing)
280+
ln = @__LINE__() - 2
281+
fl = Symbol(@__FILE__())
282+
@test length(dispatch_backtraces) == 4 # 2 ci-backtrace pairs, stored as 4 separate elements
283+
mcallee, mcaller = only(methods(callee)), only(methods(caller))
284+
# Extract pairs from the flattened array format: ci at odd indices, backtrace at even indices
285+
pairs = [(dispatch_backtraces[i], dispatch_backtraces[i+1]) for i in 1:2:length(dispatch_backtraces)]
286+
@test any(pairs) do (ci, trace)
287+
# trace is a SimpleVector from jl_backtrace_from_here, need to reformat before stacktrace
288+
bt = Base._reformat_bt(trace[1], trace[2])
289+
ci.def.def === mcallee && any(stacktrace(bt)) do sf
290+
sf.file == fl && sf.line == ln
291+
end
292+
end
293+
@test any(pairs) do (ci, trace)
294+
# trace is a SimpleVector from jl_backtrace_from_here, need to reformat before stacktrace
295+
bt = Base._reformat_bt(trace[1], trace[2])
296+
ci.def.def === mcaller && any(stacktrace(bt)) do sf
297+
sf.file == fl && sf.line == ln
298+
end
299+
end
300+
end

0 commit comments

Comments
 (0)