diff --git a/src/jlgen.jl b/src/jlgen.jl index 424b3f00..8db65c89 100644 --- a/src/jlgen.jl +++ b/src/jlgen.jl @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/test/native.jl b/test/native.jl index 95980c8e..6aebba91 100644 --- a/test/native.jl +++ b/test/native.jl @@ -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 @@ -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