Skip to content

Commit c740f2e

Browse files
maleadtclaude
andauthored
Cover the function definition line of device code. (#866)
The :code_coverage_effect expressions only mark body statements; Julia's codegen visits the signature line separately at the function prologue. Mirror that, or the header shows as missed while the body is hit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9f2c1bd commit c740f2e

2 files changed

Lines changed: 70 additions & 20 deletions

File tree

src/jlgen.jl

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -715,9 +715,20 @@ end
715715
# Julia runtime, so instead we mark those lines as visited when compiling them: Coverage of
716716
# device code thus means "this code was compiled", with counts reflecting the number of
717717
# compilations rather than executions.
718-
function record_coverage(src::CodeInfo)
718+
function coverage_visit_line(@nospecialize(file), line::Integer)
719+
file isa Symbol || return
720+
filename = String(file)
721+
(isempty(filename) || line <= 0) && return
722+
ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint),
723+
filename, ncodeunits(filename), line)
724+
return
725+
end
726+
727+
function record_coverage(mi::MethodInstance, src::CodeInfo)
728+
tracked = false
719729
for (pc, stmt) in enumerate(src.code)
720730
(stmt isa Expr && stmt.head === :code_coverage_effect) || continue
731+
tracked = true
721732
@static if VERSION >= v"1.12-"
722733
scopes = Base.IRShow.buildLineInfoNode(src.debuginfo, nothing, pc)
723734
isempty(scopes) && continue
@@ -729,11 +740,17 @@ function record_coverage(src::CodeInfo)
729740
loc = src.linetable[lineidx]::Core.LineInfoNode
730741
file, line = loc.file, loc.line
731742
end
732-
file isa Symbol || continue
733-
filename = String(file)
734-
(isempty(filename) || line <= 0) && continue
735-
ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint),
736-
filename, ncodeunits(filename), line)
743+
coverage_visit_line(file, line)
744+
end
745+
746+
# the definition (signature) line isn't a `:code_coverage_effect`; Julia's codegen
747+
# visits it separately at the prologue. Mirror that, gated on the body being tracked,
748+
# or the signature reads as missed while the body is hit.
749+
if tracked
750+
def = mi.def
751+
if def isa Method
752+
coverage_visit_line(def.file, def.line)
753+
end
737754
end
738755
return
739756
end
@@ -760,7 +777,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob))
760777
# `ci_cache_populate` does not return sources, this happens after codegen instead)
761778
if Base.JLOptions().code_coverage != 0
762779
for (ci, src) in populated
763-
record_coverage(src)
780+
record_coverage(ci.def::MethodInstance, src)
764781
end
765782
end
766783

@@ -1017,7 +1034,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob))
10171034
src = Base._uncompressed_ir(ci, src)
10181035
end
10191036
if src isa CodeInfo
1020-
record_coverage(src)
1037+
record_coverage(ci.def::MethodInstance, src)
10211038
end
10221039
end
10231040
end

test/native.jl

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ end
229229
@inline inlined_callee(x) = x + one(x)
230230
@noinline noinline_callee(x) = x * 2
231231
entry(x) = noinline_callee(inlined_callee(x))
232+
233+
# a genuinely multi-line function, so its definition (signature) line is
234+
# distinct from its body lines; compiled as its own entry below.
235+
function multiline(x)
236+
y = x + 1
237+
z = y * 2
238+
return z
239+
end
232240
end
233241

234242
# whether any line in `lo:hi` of `file` has a nonzero execution count in an
@@ -248,24 +256,49 @@ end
248256
return false
249257
end
250258

259+
# the execution count recorded for an exact line of `file`, or `nothing` if that
260+
# line was not instrumented
261+
function lcov_line_count(tracefile, file, line)
262+
in_block = false
263+
for l in eachline(tracefile)
264+
if startswith(l, "SF:")
265+
in_block = (l == "SF:" * file)
266+
elseif l == "end_of_record"
267+
in_block = false
268+
elseif in_block && startswith(l, "DA:")
269+
ln, cnt = parse.(Int, split(l[4:end], ","))
270+
ln == line && return cnt
271+
end
272+
end
273+
return nothing
274+
end
275+
251276
if Base.JLOptions().code_coverage == 0
252277
@test_skip "requires --code-coverage"
253278
else
254-
job, _ = Native.create_job(mod.entry, (Int,))
255-
JuliaContext() do ctx
256-
GPUCompiler.compile(:asm, job)
279+
for entry in (mod.entry, mod.multiline)
280+
job, _ = Native.create_job(entry, (Int,))
281+
JuliaContext() do ctx
282+
GPUCompiler.compile(:asm, job)
283+
end
257284
end
258285

259-
# flush coverage data in-process; the device functions must show covered
260-
# lines even though they were never executed by the host.
261-
mktempdir() do dir
262-
tracefile = joinpath(dir, "coverage.info")
263-
ccall(:jl_write_coverage_data, Cvoid, (Cstring,), tracefile)
264-
for f in (mod.inlined_callee, mod.noinline_callee, mod.entry)
265-
m = only(methods(f))
266-
@test lcov_any_covered(tracefile, string(m.file), m.line, m.line + 1)
267-
end
286+
# flush coverage in-process; device lines show covered despite never running.
287+
# bare mktempdir (cleaned at exit, after a GC) dodges the EBUSY `rm` race the
288+
# `do` form hits on Windows. jl_write_coverage_data needs a `.info` path.
289+
dir = mktempdir()
290+
tracefile = joinpath(dir, "coverage.info")
291+
ccall(:jl_write_coverage_data, Cvoid, (Cstring,), tracefile)
292+
for f in (mod.inlined_callee, mod.noinline_callee, mod.entry)
293+
m = only(methods(f))
294+
@test lcov_any_covered(tracefile, string(m.file), m.line, m.line + 1)
268295
end
296+
297+
# the definition line must be covered too, not just the body (Julia covers
298+
# it separately at the prologue)
299+
m = only(methods(mod.multiline))
300+
@test lcov_line_count(tracefile, string(m.file), m.line) !== nothing
301+
@test something(lcov_line_count(tracefile, string(m.file), m.line), 0) >= 1
269302
end
270303
end
271304
end

0 commit comments

Comments
 (0)