minicov/lib.rs
1//! This crate provides code coverage and profile-guided optimization (PGO) support
2//! for `no_std` and embedded programs.
3//!
4//! This is done through a modified version of the LLVM profiling runtime (normally
5//! part of compiler-rt) from which all dependencies on libc have been removed.
6//!
7//! All types of instrumentation using the LLVM profiling runtime are supported:
8//! - Rust code coverage with `-Cinstrument-coverage`.
9//! - Rust profile-guided optimization with `-Cprofile-generate`.
10//! - Clang code coverage with `-fprofile-instr-generate -fcoverage-mapping`.
11//! - Clang profile-guided optimization with `-fprofile-instr-generate`.
12//! - Clang LLVM IR profile-guided optimization with `-fprofile-generate`.
13//!
14//! Note that to profile both C and Rust code at the same time you must use Clang
15//! with the same LLVM version as the LLVM used by rustc. You can pass these flags
16//! to C code compiled with the `cc` crates using [environment variables].
17//!
18//! [environment variables]: https://github.com/rust-lang/cc-rs#external-configuration-via-environment-variables
19//!
20//! ## Usage
21//!
22//! Note: This crate requires a recent nightly compiler.
23//!
24//! 1. Ensure that the following environment variables are set up:
25//!
26//! ```sh
27//! export RUSTFLAGS="-Cinstrument-coverage -Zno-profiler-runtime"
28//! ```
29//!
30//! Note that these flags also apply to build-dependencies and proc
31//! macros by default. This can be worked around by explicitly
32//! specifying a target when invoking cargo:
33//!
34//! ```sh
35//! # Applies RUSTFLAGS to everything
36//! cargo build
37//!
38//! # Doesn't apply RUSTFLAGS to build dependencies and proc macros
39//! cargo build --target x86_64-unknown-linux-gnu
40//! ```
41//!
42//! 2. Add the `minicov` crate as a dependency to your program:
43//!
44//! ```toml
45//! [dependencies]
46//! minicov = "0.3"
47//! ```
48//!
49//! 3. Before your program exits, call `minicov::capture_coverage` with a sink (such
50//! as `Vec<u8>`) and then dump its contents to a file with the `.profraw` extension:
51//!
52//! ```ignore
53//! fn main() {
54//! // ...
55//!
56//! let mut coverage = vec![];
57//! unsafe {
58//! // Note that this function is not thread-safe! Use a lock if needed.
59//! minicov::capture_coverage(&mut coverage).unwrap();
60//! }
61//! std::fs::write("output.profraw", coverage).unwrap();
62//! }
63//! ```
64//!
65//! If your program is running on a different system than your build system then
66//! you will need to transfer this file back to your build system.
67//!
68//! Sinks must implement the `CoverageWriter` trait. If the default `alloc` feature
69//! is enabled then an implementation is provided for `Vec<u8>`.
70//!
71//! 4. Use a tool such as [grcov] or llvm-cov to generate a human-readable coverage
72//! report:
73//!
74//! ```sh
75//! grcov output.profraw -b ./target/debug/my_program -s . -t html -o cov_report
76//! ```
77//!
78//! [grcov]: https://github.com/mozilla/grcov
79//!
80//! ## Profile-guided optimization
81//!
82//! The steps for profile-guided optimzation are similar. The only difference is the
83//! flags passed in `RUSTFLAGS`:
84//!
85//! ```sh
86//! # First run to generate profiling information.
87//! export RUSTFLAGS="-Cprofile-generate -Zno-profiler-runtime"
88//! cargo run --target x86_64-unknown-linux-gnu --release
89//!
90//! # Post-process the profiling information.
91//! # The rust-profdata tool comes from cargo-binutils.
92//! rust-profdata merge -o output.profdata output.profraw
93//!
94//! # Optimized build using PGO. minicov is not needed in this step.
95//! export RUSTFLAGS="-Cprofile-use=output.profdata"
96//! cargo build --target x86_64-unknown-linux-gnu --release
97//! ```
98
99#![no_std]
100#![warn(missing_docs)]
101#![warn(rust_2018_idioms)]
102
103#[cfg(feature = "alloc")]
104extern crate alloc;
105
106#[cfg(feature = "alloc")]
107use alloc::vec::Vec;
108#[cfg(feature = "alloc")]
109use core::alloc::Layout;
110use core::{fmt, slice};
111
112#[allow(non_snake_case)]
113#[repr(C)]
114struct ProfDataIOVec {
115 Data: *mut u8,
116 ElmSize: usize,
117 NumElm: usize,
118 UseZeroPadding: i32,
119}
120
121#[allow(non_snake_case)]
122#[repr(C)]
123struct ProfDataWriter {
124 Write:
125 unsafe extern "C" fn(This: *mut ProfDataWriter, *mut ProfDataIOVec, NumIOVecs: u32) -> u32,
126 WriterCtx: *mut u8,
127}
128
129// Opaque type for our purposes.
130enum VPDataReaderType {}
131
132extern "C" {
133 fn __llvm_profile_reset_counters();
134 fn __llvm_profile_merge_from_buffer(profile: *const u8, size: u64) -> i32;
135 fn __llvm_profile_check_compatibility(profile: *const u8, size: u64) -> i32;
136 fn __llvm_profile_get_version() -> u64;
137 fn lprofWriteData(
138 Writer: *mut ProfDataWriter,
139 VPDataReader: *mut VPDataReaderType,
140 SkipNameDataWrite: i32,
141 ) -> i32;
142 fn lprofGetVPDataReader() -> *mut VPDataReaderType;
143}
144
145const INSTR_PROF_RAW_VERSION: u64 = 10;
146const VARIANT_MASKS_ALL: u64 = 0xffffffff00000000;
147
148// On some target rustc will insert an artificial dependency on the
149// __llvm_profile_runtime symbol to ensure the static initializer from LLVM's
150// profiling runtime is pulled in by the linker. We don't need any runtime
151// initialization so we just provide the symbol here.
152#[no_mangle]
153static __llvm_profile_runtime: u8 = 0;
154
155// Memory allocation functions used by value profiling. If the "alloc" feature
156// is disabled then value profiling will also be disabled.
157#[cfg(feature = "alloc")]
158#[no_mangle]
159unsafe fn minicov_alloc_zeroed(size: usize, align: usize) -> *mut u8 {
160 alloc::alloc::alloc_zeroed(Layout::from_size_align(size, align).unwrap())
161}
162#[cfg(feature = "alloc")]
163#[no_mangle]
164unsafe extern "C" fn minicov_dealloc(ptr: *mut u8, size: usize, align: usize) {
165 alloc::alloc::dealloc(ptr, Layout::from_size_align(size, align).unwrap())
166}
167#[cfg(not(feature = "alloc"))]
168#[no_mangle]
169unsafe fn minicov_alloc_zeroed(_size: usize, _align: usize) -> *mut u8 {
170 core::ptr::null_mut()
171}
172#[cfg(not(feature = "alloc"))]
173#[no_mangle]
174unsafe extern "C" fn minicov_dealloc(_ptr: *mut u8, _size: usize, _align: usize) {}
175
176/// Sink into which coverage data can be written.
177///
178/// A default implementation for `Vec<u8>` is provided,
179pub trait CoverageWriter {
180 /// Writes the given bytes to the sink.
181 ///
182 /// This method should return an error if all bytes could not be written to
183 /// the sink.
184 fn write(&mut self, data: &[u8]) -> Result<(), CoverageWriteError>;
185}
186
187#[cfg(feature = "alloc")]
188impl CoverageWriter for Vec<u8> {
189 fn write(&mut self, data: &[u8]) -> Result<(), CoverageWriteError> {
190 self.extend_from_slice(data);
191 Ok(())
192 }
193}
194
195/// Callback function passed to `lprofWriteData`.
196unsafe extern "C" fn write_callback<Writer: CoverageWriter>(
197 this: *mut ProfDataWriter,
198 iovecs: *mut ProfDataIOVec,
199 num_iovecs: u32,
200) -> u32 {
201 let writer = &mut *((*this).WriterCtx as *mut Writer);
202 for iov in slice::from_raw_parts(iovecs, num_iovecs as usize) {
203 let len = iov.ElmSize * iov.NumElm;
204 if iov.Data.is_null() {
205 // Pad with zero bytes.
206 let zero = [0; 16];
207 let mut remaining = len;
208 while remaining != 0 {
209 let data = &zero[..usize::min(zero.len(), remaining)];
210 if writer.write(data).is_err() {
211 return 1;
212 }
213 remaining -= data.len();
214 }
215 } else {
216 let data = slice::from_raw_parts(iov.Data, len);
217 if writer.write(data).is_err() {
218 return 1;
219 }
220 }
221 }
222 0
223}
224
225/// Checks that the instrumented binary uses the same profiling data format as
226/// the LLVM profiling runtime.
227fn check_version() {
228 let version = unsafe { __llvm_profile_get_version() & !VARIANT_MASKS_ALL };
229 assert_eq!(
230 version, INSTR_PROF_RAW_VERSION,
231 "Runtime and instrumentation version mismatch"
232 );
233}
234
235/// Captures the coverage data for the current program and writes it into the
236/// given sink.
237///
238/// The data should be saved to a file with the `.profraw` extension, which can
239/// then be processed using the `llvm-profdata` and `llvm-cov` tools.
240///
241/// You should call `reset_coverage` afterwards if you intend to continue
242/// running the program so that future coverage can be merged with the returned
243/// captured coverage.
244///
245/// # Safety
246///
247/// This function is not thread-safe and should not be concurrently called from
248/// multiple threads.
249pub unsafe fn capture_coverage<Writer: CoverageWriter>(
250 writer: &mut Writer,
251) -> Result<(), CoverageWriteError> {
252 check_version();
253
254 let mut prof_writer = ProfDataWriter {
255 Write: write_callback::<Writer>,
256 WriterCtx: writer as *mut Writer as *mut u8,
257 };
258 let res = lprofWriteData(&mut prof_writer, lprofGetVPDataReader(), 0);
259 if res == 0 {
260 Ok(())
261 } else {
262 Err(CoverageWriteError)
263 }
264}
265
266/// Error type returned when trying to merge incompatible coverage data.
267///
268/// This typically happens if the coverage data comes from a different binary.
269#[derive(Copy, Clone, Debug)]
270pub struct IncompatibleCoverageData;
271impl fmt::Display for IncompatibleCoverageData {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.write_str("incompatible coverage data")
274 }
275}
276
277/// Error while trying to write coverage data.
278///
279/// This only happens if the `CoverageWriter` implementation returns an error.
280#[derive(Copy, Clone, Debug)]
281pub struct CoverageWriteError;
282impl fmt::Display for CoverageWriteError {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 f.write_str("error while writing coverage data")
285 }
286}
287
288/// Merges previously dumped coverage data into the coverage counters.
289///
290/// This should be called prior to dumping if coverage data from a previous run
291/// already exists and should be merged with instead of overwritten.
292///
293/// # Safety
294///
295/// This function is not thread-safe and should not be concurrently called from
296/// multiple threads.
297pub unsafe fn merge_coverage(data: &[u8]) -> Result<(), IncompatibleCoverageData> {
298 check_version();
299
300 if __llvm_profile_check_compatibility(data.as_ptr(), data.len() as u64) == 0
301 && __llvm_profile_merge_from_buffer(data.as_ptr(), data.len() as u64) == 0
302 {
303 Ok(())
304 } else {
305 Err(IncompatibleCoverageData)
306 }
307}
308
309/// Resets all coverage counters in the program to zero.
310///
311/// This function should be called after a process forks to avoid recording
312/// coverage data for the parent process twice.
313///
314/// You should also call this after calling `capture_coverage` if you intend to
315/// continue running with the intention of merging with the captured coverage
316/// later.
317pub fn reset_coverage() {
318 check_version();
319
320 unsafe {
321 __llvm_profile_reset_counters();
322 }
323}