Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions src/jlgen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,20 @@ end
# Julia runtime, so instead we mark those lines as visited when compiling them: Coverage of
# device code thus means "this code was compiled", with counts reflecting the number of
# compilations rather than executions.
function record_coverage(src::CodeInfo)
function coverage_visit_line(@nospecialize(file), line::Integer)
file isa Symbol || return
filename = String(file)
(isempty(filename) || line <= 0) && return
ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint),
filename, ncodeunits(filename), line)
return
end

function record_coverage(mi::MethodInstance, src::CodeInfo)
tracked = false
for (pc, stmt) in enumerate(src.code)
(stmt isa Expr && stmt.head === :code_coverage_effect) || continue
tracked = true
@static if VERSION >= v"1.12-"
scopes = Base.IRShow.buildLineInfoNode(src.debuginfo, nothing, pc)
isempty(scopes) && continue
Expand All @@ -729,11 +740,17 @@ function record_coverage(src::CodeInfo)
loc = src.linetable[lineidx]::Core.LineInfoNode
file, line = loc.file, loc.line
end
file isa Symbol || continue
filename = String(file)
(isempty(filename) || line <= 0) && continue
ccall(:jl_coverage_visit_line, Cvoid, (Cstring, Csize_t, Cint),
filename, ncodeunits(filename), line)
coverage_visit_line(file, line)
end

# the definition (signature) line isn't a `:code_coverage_effect`; Julia's codegen
# visits it separately at the prologue. Mirror that, gated on the body being tracked,
# or the signature reads as missed while the body is hit.
if tracked
def = mi.def
if def isa Method
coverage_visit_line(def.file, def.line)
end
end
return
end
Expand All @@ -760,7 +777,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob))
# `ci_cache_populate` does not return sources, this happens after codegen instead)
if Base.JLOptions().code_coverage != 0
for (ci, src) in populated
record_coverage(src)
record_coverage(ci.def::MethodInstance, src)
end
end

Expand Down Expand Up @@ -1017,7 +1034,7 @@ function compile_method_instance(@nospecialize(job::CompilerJob))
src = Base._uncompressed_ir(ci, src)
end
if src isa CodeInfo
record_coverage(src)
record_coverage(ci.def::MethodInstance, src)
end
end
end
Expand Down
57 changes: 45 additions & 12 deletions test/native.jl
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ end
@inline inlined_callee(x) = x + one(x)
@noinline noinline_callee(x) = x * 2
entry(x) = noinline_callee(inlined_callee(x))

# a genuinely multi-line function, so its definition (signature) line is
# distinct from its body lines; compiled as its own entry below.
function multiline(x)
y = x + 1
z = y * 2
return z
end
end

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

# the execution count recorded for an exact line of `file`, or `nothing` if that
# line was not instrumented
function lcov_line_count(tracefile, file, line)
in_block = false
for l in eachline(tracefile)
if startswith(l, "SF:")
in_block = (l == "SF:" * file)
elseif l == "end_of_record"
in_block = false
elseif in_block && startswith(l, "DA:")
ln, cnt = parse.(Int, split(l[4:end], ","))
ln == line && return cnt
end
end
return nothing
end

if Base.JLOptions().code_coverage == 0
@test_skip "requires --code-coverage"
else
job, _ = Native.create_job(mod.entry, (Int,))
JuliaContext() do ctx
GPUCompiler.compile(:asm, job)
for entry in (mod.entry, mod.multiline)
job, _ = Native.create_job(entry, (Int,))
JuliaContext() do ctx
GPUCompiler.compile(:asm, job)
end
end

# flush coverage data in-process; the device functions must show covered
# lines even though they were never executed by the host.
mktempdir() do dir
tracefile = joinpath(dir, "coverage.info")
ccall(:jl_write_coverage_data, Cvoid, (Cstring,), tracefile)
for f in (mod.inlined_callee, mod.noinline_callee, mod.entry)
m = only(methods(f))
@test lcov_any_covered(tracefile, string(m.file), m.line, m.line + 1)
end
# flush coverage in-process; device lines show covered despite never running.
# bare mktempdir (cleaned at exit, after a GC) dodges the EBUSY `rm` race the
# `do` form hits on Windows. jl_write_coverage_data needs a `.info` path.
dir = mktempdir()
tracefile = joinpath(dir, "coverage.info")
ccall(:jl_write_coverage_data, Cvoid, (Cstring,), tracefile)
for f in (mod.inlined_callee, mod.noinline_callee, mod.entry)
m = only(methods(f))
@test lcov_any_covered(tracefile, string(m.file), m.line, m.line + 1)
end

# the definition line must be covered too, not just the body (Julia covers
# it separately at the prologue)
m = only(methods(mod.multiline))
@test lcov_line_count(tracefile, string(m.file), m.line) !== nothing
@test something(lcov_line_count(tracefile, string(m.file), m.line), 0) >= 1
end
end
end
Expand Down
Loading