about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRich Kadel <richkadel@google.com>2020-10-22 20:28:16 -0700
committerRich Kadel <richkadel@google.com>2020-11-05 18:24:14 -0800
commit3291d28e9ac1173f033d240bc5f3f145c9c8dd59 (patch)
tree9434df1af15a71dec68b47ed307e07a0d40efebb
parentb5020648fe294a1f139586e4243903d8c1a105b8 (diff)
downloadrust-3291d28e9ac1173f033d240bc5f3f145c9c8dd59.tar.gz
rust-3291d28e9ac1173f033d240bc5f3f145c9c8dd59.zip
Adds coverage graphviz
-rw-r--r--compiler/rustc_mir/src/transform/coverage/counters.rs37
-rw-r--r--compiler/rustc_mir/src/transform/coverage/debug.rs365
-rw-r--r--compiler/rustc_mir/src/transform/coverage/mod.rs34
-rw-r--r--compiler/rustc_mir/src/transform/coverage/spans.rs8
-rw-r--r--compiler/rustc_mir/src/util/generic_graphviz.rs185
-rw-r--r--compiler/rustc_mir/src/util/mod.rs1
-rw-r--r--src/test/mir-opt/coverage_graphviz.bar.InstrumentCoverage.0.dot6
-rw-r--r--src/test/mir-opt/coverage_graphviz.main.InstrumentCoverage.0.dot11
-rw-r--r--src/test/mir-opt/coverage_graphviz.rs20
9 files changed, 657 insertions, 10 deletions
diff --git a/compiler/rustc_mir/src/transform/coverage/counters.rs b/compiler/rustc_mir/src/transform/coverage/counters.rs
index 511ad937c24..c31f401780e 100644
--- a/compiler/rustc_mir/src/transform/coverage/counters.rs
+++ b/compiler/rustc_mir/src/transform/coverage/counters.rs
@@ -1,3 +1,7 @@
+use super::debug;
+
+use debug::DebugCounters;
+
 use rustc_middle::mir::coverage::*;
 
 /// Manages the counter and expression indexes/IDs to generate `CoverageKind` components for MIR
@@ -6,6 +10,7 @@ pub(crate) struct CoverageCounters {
     function_source_hash: u64,
     next_counter_id: u32,
     num_expressions: u32,
+    pub debug_counters: DebugCounters,
 }
 
 impl CoverageCounters {
@@ -14,24 +19,46 @@ impl CoverageCounters {
             function_source_hash,
             next_counter_id: CounterValueReference::START.as_u32(),
             num_expressions: 0,
+            debug_counters: DebugCounters::new(),
         }
     }
 
-    pub fn make_counter(&mut self) -> CoverageKind {
-        CoverageKind::Counter {
+    /// Activate the `DebugCounters` data structures, to provide additional debug formatting
+    /// features when formating `CoverageKind` (counter) values.
+    pub fn enable_debug(&mut self) {
+        self.debug_counters.enable();
+    }
+
+    pub fn make_counter<F>(&mut self, debug_block_label_fn: F) -> CoverageKind
+    where
+        F: Fn() -> Option<String>,
+    {
+        let counter = CoverageKind::Counter {
             function_source_hash: self.function_source_hash,
             id: self.next_counter(),
+        };
+        if self.debug_counters.is_enabled() {
+            self.debug_counters.add_counter(&counter, (debug_block_label_fn)());
         }
+        counter
     }
 
-    pub fn make_expression(
+    pub fn make_expression<F>(
         &mut self,
         lhs: ExpressionOperandId,
         op: Op,
         rhs: ExpressionOperandId,
-    ) -> CoverageKind {
+        debug_block_label_fn: F,
+    ) -> CoverageKind
+    where
+        F: Fn() -> Option<String>,
+    {
         let id = self.next_expression();
-        CoverageKind::Expression { id, lhs, op, rhs }
+        let expression = CoverageKind::Expression { id, lhs, op, rhs };
+        if self.debug_counters.is_enabled() {
+            self.debug_counters.add_counter(&expression, (debug_block_label_fn)());
+        }
+        expression
     }
 
     /// Counter IDs start from one and go up.
diff --git a/compiler/rustc_mir/src/transform/coverage/debug.rs b/compiler/rustc_mir/src/transform/coverage/debug.rs
index 7eb2d33453c..fbf2ad224a8 100644
--- a/compiler/rustc_mir/src/transform/coverage/debug.rs
+++ b/compiler/rustc_mir/src/transform/coverage/debug.rs
@@ -1,12 +1,294 @@
-use super::graph::CoverageGraph;
+use super::graph::{BasicCoverageBlock, BasicCoverageBlockData, CoverageGraph};
 use super::spans::CoverageSpan;
 
+use crate::util::generic_graphviz::GraphvizWriter;
 use crate::util::pretty;
 use crate::util::spanview::{self, SpanViewable};
 
-use rustc_middle::mir::{self, TerminatorKind};
+use rustc_data_structures::fx::FxHashMap;
+use rustc_index::vec::Idx;
+use rustc_middle::mir::coverage::*;
+use rustc_middle::mir::{self, BasicBlock, TerminatorKind};
 use rustc_middle::ty::TyCtxt;
 
+use std::lazy::SyncOnceCell;
+
+const RUSTC_COVERAGE_DEBUG_OPTIONS: &str = "RUSTC_COVERAGE_DEBUG_OPTIONS";
+
+pub(crate) fn debug_options<'a>() -> &'a DebugOptions {
+    static DEBUG_OPTIONS: SyncOnceCell<DebugOptions> = SyncOnceCell::new();
+
+    &DEBUG_OPTIONS.get_or_init(|| DebugOptions::new())
+}
+
+/// Parses and maintains coverage-specific debug options captured from the environment variable
+/// "RUSTC_COVERAGE_DEBUG_OPTIONS", if set. Options can be set on the command line by, for example:
+///
+///     $ RUSTC_COVERAGE_DEBUG_OPTIONS=counter-format=block cargo build
+#[derive(Debug, Clone)]
+pub(crate) struct DebugOptions {
+    counter_format: ExpressionFormat,
+}
+
+impl DebugOptions {
+    fn new() -> Self {
+        let mut counter_format = ExpressionFormat::default();
+
+        if let Ok(env_debug_options) = std::env::var(RUSTC_COVERAGE_DEBUG_OPTIONS) {
+            for setting_str in env_debug_options.replace(" ", "").replace("-", "_").split(",") {
+                let mut setting = setting_str.splitn(2, "=");
+                match setting.next() {
+                    Some(option) if option == "counter_format" => {
+                        if let Some(strval) = setting.next() {
+                            counter_format = counter_format_option_val(strval);
+                            debug!(
+                                "{} env option `counter_format` is set to {:?}",
+                                RUSTC_COVERAGE_DEBUG_OPTIONS, counter_format
+                            );
+                        } else {
+                            bug!(
+                                "`{}` option in environment variable {} requires one or more \
+                                plus-separated choices (a non-empty subset of \
+                                `id+block+operation`)",
+                                option,
+                                RUSTC_COVERAGE_DEBUG_OPTIONS
+                            );
+                        }
+                    }
+                    Some("") => {}
+                    Some(invalid) => bug!(
+                        "Unsupported setting `{}` in environment variable {}",
+                        invalid,
+                        RUSTC_COVERAGE_DEBUG_OPTIONS
+                    ),
+                    None => {}
+                }
+            }
+        }
+
+        Self { counter_format }
+    }
+}
+
+fn counter_format_option_val(strval: &str) -> ExpressionFormat {
+    let mut counter_format = ExpressionFormat { id: false, block: false, operation: false };
+    let components = strval.splitn(3, "+");
+    for component in components {
+        match component {
+            "id" => counter_format.id = true,
+            "block" => counter_format.block = true,
+            "operation" => counter_format.operation = true,
+            _ => bug!(
+                "Unsupported counter_format choice `{}` in environment variable {}",
+                component,
+                RUSTC_COVERAGE_DEBUG_OPTIONS
+            ),
+        }
+    }
+    counter_format
+}
+
+#[derive(Debug, Clone)]
+struct ExpressionFormat {
+    id: bool,
+    block: bool,
+    operation: bool,
+}
+
+impl Default for ExpressionFormat {
+    fn default() -> Self {
+        Self { id: false, block: true, operation: true }
+    }
+}
+
+/// If enabled, this struct maintains a map from `CoverageKind` IDs (as `ExpressionOperandId`) to
+/// the `CoverageKind` data and optional label (normally, the counter's associated
+/// `BasicCoverageBlock` format string, if any).
+///
+/// Use `format_counter` to convert one of these `CoverageKind` counters to a debug output string,
+/// as directed by the `DebugOptions`. This allows the format of counter labels in logs and dump
+/// files (including the `CoverageGraph` graphviz file) to be changed at runtime, via environment
+/// variable.
+///
+/// `DebugCounters` supports a recursive rendering of `Expression` counters, so they can be
+/// presented as nested expressions such as `(bcb3 - (bcb0 + bcb1))`.
+pub(crate) struct DebugCounters {
+    some_counters: Option<FxHashMap<ExpressionOperandId, DebugCounter>>,
+}
+
+impl DebugCounters {
+    pub fn new() -> Self {
+        Self { some_counters: None }
+    }
+
+    pub fn enable(&mut self) {
+        self.some_counters.replace(FxHashMap::default());
+    }
+
+    pub fn is_enabled(&mut self) -> bool {
+        self.some_counters.is_some()
+    }
+
+    pub fn add_counter(&mut self, counter_kind: &CoverageKind, some_block_label: Option<String>) {
+        if let Some(counters) = &mut self.some_counters {
+            let id: ExpressionOperandId = match *counter_kind {
+                CoverageKind::Counter { id, .. } => id.into(),
+                CoverageKind::Expression { id, .. } => id.into(),
+                _ => bug!(
+                    "the given `CoverageKind` is not an counter or expression: {:?}",
+                    counter_kind
+                ),
+            };
+            counters
+                .insert(id.into(), DebugCounter::new(counter_kind.clone(), some_block_label))
+                .expect_none(
+                    "attempt to add the same counter_kind to DebugCounters more than once",
+                );
+        }
+    }
+
+    pub fn format_counter(&self, counter_kind: &CoverageKind) -> String {
+        match *counter_kind {
+            CoverageKind::Counter { .. } => {
+                format!("Counter({})", self.format_counter_kind(counter_kind))
+            }
+            CoverageKind::Expression { .. } => {
+                format!("Expression({})", self.format_counter_kind(counter_kind))
+            }
+            CoverageKind::Unreachable { .. } => "Unreachable".to_owned(),
+        }
+    }
+
+    fn format_counter_kind(&self, counter_kind: &CoverageKind) -> String {
+        let counter_format = &debug_options().counter_format;
+        if let CoverageKind::Expression { id, lhs, op, rhs } = *counter_kind {
+            if counter_format.operation {
+                return format!(
+                    "{}{} {} {}",
+                    if counter_format.id || self.some_counters.is_none() {
+                        format!("#{} = ", id.index())
+                    } else {
+                        String::new()
+                    },
+                    self.format_operand(lhs),
+                    if op == Op::Add { "+" } else { "-" },
+                    self.format_operand(rhs),
+                );
+            }
+        }
+
+        let id: ExpressionOperandId = match *counter_kind {
+            CoverageKind::Counter { id, .. } => id.into(),
+            CoverageKind::Expression { id, .. } => id.into(),
+            _ => {
+                bug!("the given `CoverageKind` is not an counter or expression: {:?}", counter_kind)
+            }
+        };
+        if self.some_counters.is_some() && (counter_format.block || !counter_format.id) {
+            let counters = self.some_counters.as_ref().unwrap();
+            if let Some(DebugCounter { some_block_label: Some(block_label), .. }) =
+                counters.get(&id.into())
+            {
+                return if counter_format.id {
+                    format!("{}#{}", block_label, id.index())
+                } else {
+                    format!("{}", block_label)
+                };
+            }
+        }
+        format!("#{}", id.index())
+    }
+
+    fn format_operand(&self, operand: ExpressionOperandId) -> String {
+        if operand.index() == 0 {
+            return String::from("0");
+        }
+        if let Some(counters) = &self.some_counters {
+            if let Some(DebugCounter { counter_kind, some_block_label }) = counters.get(&operand) {
+                if let CoverageKind::Expression { .. } = counter_kind {
+                    if let Some(block_label) = some_block_label {
+                        if debug_options().counter_format.block {
+                            return format!(
+                                "{}:({})",
+                                block_label,
+                                self.format_counter_kind(counter_kind)
+                            );
+                        }
+                    }
+                    return format!("({})", self.format_counter_kind(counter_kind));
+                }
+                return format!("{}", self.format_counter_kind(counter_kind));
+            }
+        }
+        format!("#{}", operand.index().to_string())
+    }
+}
+
+/// A non-public support class to `DebugCounters`.
+#[derive(Debug)]
+struct DebugCounter {
+    counter_kind: CoverageKind,
+    some_block_label: Option<String>,
+}
+
+impl DebugCounter {
+    fn new(counter_kind: CoverageKind, some_block_label: Option<String>) -> Self {
+        Self { counter_kind, some_block_label }
+    }
+}
+
+/// If enabled, this data structure captures additional debugging information used when generating
+/// a Graphviz (.dot file) representation of the `CoverageGraph`, for debugging purposes.
+pub(crate) struct GraphvizData {
+    some_bcb_to_coverage_spans_with_counters:
+        Option<FxHashMap<BasicCoverageBlock, Vec<(CoverageSpan, CoverageKind)>>>,
+    some_edge_to_counter: Option<FxHashMap<(BasicCoverageBlock, BasicBlock), CoverageKind>>,
+}
+
+impl GraphvizData {
+    pub fn new() -> Self {
+        Self { some_bcb_to_coverage_spans_with_counters: None, some_edge_to_counter: None }
+    }
+
+    pub fn enable(&mut self) {
+        self.some_bcb_to_coverage_spans_with_counters = Some(FxHashMap::default());
+        self.some_edge_to_counter = Some(FxHashMap::default());
+    }
+
+    pub fn is_enabled(&mut self) -> bool {
+        self.some_bcb_to_coverage_spans_with_counters.is_some()
+    }
+
+    pub fn add_bcb_coverage_span_with_counter(
+        &mut self,
+        bcb: BasicCoverageBlock,
+        coverage_span: &CoverageSpan,
+        counter_kind: &CoverageKind,
+    ) {
+        if let Some(bcb_to_coverage_spans_with_counters) =
+            self.some_bcb_to_coverage_spans_with_counters.as_mut()
+        {
+            bcb_to_coverage_spans_with_counters
+                .entry(bcb)
+                .or_insert_with(|| Vec::new())
+                .push((coverage_span.clone(), counter_kind.clone()));
+        }
+    }
+
+    pub fn get_bcb_coverage_spans_with_counters(
+        &self,
+        bcb: BasicCoverageBlock,
+    ) -> Option<&Vec<(CoverageSpan, CoverageKind)>> {
+        if let Some(bcb_to_coverage_spans_with_counters) =
+            self.some_bcb_to_coverage_spans_with_counters.as_ref()
+        {
+            bcb_to_coverage_spans_with_counters.get(&bcb)
+        } else {
+            None
+        }
+    }
+}
+
 /// Generates the MIR pass `CoverageSpan`-specific spanview dump file.
 pub(crate) fn dump_coverage_spanview(
     tcx: TyCtxt<'tcx>,
@@ -47,6 +329,85 @@ fn span_viewables(
     span_viewables
 }
 
+/// Generates the MIR pass coverage-specific graphviz dump file.
+pub(crate) fn dump_coverage_graphviz(
+    tcx: TyCtxt<'tcx>,
+    mir_body: &mir::Body<'tcx>,
+    pass_name: &str,
+    basic_coverage_blocks: &CoverageGraph,
+    debug_counters: &DebugCounters,
+    graphviz_data: &GraphvizData,
+) {
+    let mir_source = mir_body.source;
+    let def_id = mir_source.def_id();
+    let node_content = |bcb| {
+        bcb_to_string_sections(
+            tcx,
+            mir_body,
+            debug_counters,
+            &basic_coverage_blocks[bcb],
+            graphviz_data.get_bcb_coverage_spans_with_counters(bcb),
+        )
+    };
+    let edge_labels = |from_bcb| {
+        let from_bcb_data = &basic_coverage_blocks[from_bcb];
+        let from_terminator = from_bcb_data.terminator(mir_body);
+        from_terminator
+            .kind
+            .fmt_successor_labels()
+            .iter()
+            .map(|label| label.to_string())
+            .collect::<Vec<_>>()
+    };
+    let graphviz_name = format!("Cov_{}_{}", def_id.krate.index(), def_id.index.index());
+    let graphviz_writer =
+        GraphvizWriter::new(basic_coverage_blocks, &graphviz_name, node_content, edge_labels);
+    let mut file = pretty::create_dump_file(tcx, "dot", None, pass_name, &0, mir_source)
+        .expect("Unexpected error creating BasicCoverageBlock graphviz DOT file");
+    graphviz_writer
+        .write_graphviz(tcx, &mut file)
+        .expect("Unexpected error writing BasicCoverageBlock graphviz DOT file");
+}
+
+fn bcb_to_string_sections(
+    tcx: TyCtxt<'tcx>,
+    mir_body: &mir::Body<'tcx>,
+    debug_counters: &DebugCounters,
+    bcb_data: &BasicCoverageBlockData,
+    some_coverage_spans_with_counters: Option<&Vec<(CoverageSpan, CoverageKind)>>,
+) -> Vec<String> {
+    let len = bcb_data.basic_blocks.len();
+    let mut sections = Vec::new();
+    if let Some(coverage_spans_with_counters) = some_coverage_spans_with_counters {
+        sections.push(
+            coverage_spans_with_counters
+                .iter()
+                .map(|(covspan, counter)| {
+                    format!(
+                        "{} at {}",
+                        debug_counters.format_counter(counter),
+                        covspan.format(tcx, mir_body)
+                    )
+                })
+                .collect::<Vec<_>>()
+                .join("\n"),
+        );
+    }
+    let non_term_blocks = bcb_data.basic_blocks[0..len - 1]
+        .iter()
+        .map(|&bb| format!("{:?}: {}", bb, term_type(&mir_body[bb].terminator().kind)))
+        .collect::<Vec<_>>();
+    if non_term_blocks.len() > 0 {
+        sections.push(non_term_blocks.join("\n"));
+    }
+    sections.push(format!(
+        "{:?}: {}",
+        bcb_data.basic_blocks.last().unwrap(),
+        term_type(&bcb_data.terminator(mir_body).kind)
+    ));
+    sections
+}
+
 /// Returns a simple string representation of a `TerminatorKind` variant, indenpendent of any
 /// values it might hold.
 pub(crate) fn term_type(kind: &TerminatorKind<'tcx>) -> &'static str {
diff --git a/compiler/rustc_mir/src/transform/coverage/mod.rs b/compiler/rustc_mir/src/transform/coverage/mod.rs
index aa5771cfad4..d1d94092c9d 100644
--- a/compiler/rustc_mir/src/transform/coverage/mod.rs
+++ b/compiler/rustc_mir/src/transform/coverage/mod.rs
@@ -103,6 +103,14 @@ impl<'a, 'tcx> Instrumentor<'a, 'tcx> {
 
         debug!("instrumenting {:?}, span: {}", def_id, source_map.span_to_string(body_span));
 
+        let mut graphviz_data = debug::GraphvizData::new();
+
+        let dump_graphviz = tcx.sess.opts.debugging_opts.dump_mir_graphviz;
+        if dump_graphviz {
+            graphviz_data.enable();
+            self.coverage_counters.enable_debug();
+        }
+
         ////////////////////////////////////////////////////
         // Compute `CoverageSpan`s from the `CoverageGraph`.
         let coverage_spans = CoverageSpans::generate_coverage_spans(
@@ -121,7 +129,20 @@ impl<'a, 'tcx> Instrumentor<'a, 'tcx> {
             );
         }
 
-        self.inject_coverage_span_counters(coverage_spans);
+        self.inject_coverage_span_counters(coverage_spans, &mut graphviz_data);
+
+        if graphviz_data.is_enabled() {
+            // Even if there was an error, a partial CoverageGraph can still generate a useful
+            // graphviz output.
+            debug::dump_coverage_graphviz(
+                tcx,
+                self.mir_body,
+                self.pass_name,
+                &self.basic_coverage_blocks,
+                &self.coverage_counters.debug_counters,
+                &graphviz_data,
+            );
+        }
     }
 
     /// Inject a counter for each `CoverageSpan`. There can be multiple `CoverageSpan`s for a given
@@ -129,7 +150,11 @@ impl<'a, 'tcx> Instrumentor<'a, 'tcx> {
     /// `bcb` to its `Counter`, when injected. Subsequent `CoverageSpan`s for a BCB that already has
     /// a `Counter` will inject an `Expression` instead, and compute its value by adding `ZERO` to
     /// the BCB `Counter` value.
-    fn inject_coverage_span_counters(&mut self, coverage_spans: Vec<CoverageSpan>) {
+    fn inject_coverage_span_counters(
+        &mut self,
+        coverage_spans: Vec<CoverageSpan>,
+        graphviz_data: &mut debug::GraphvizData,
+    ) {
         let tcx = self.tcx;
         let source_map = tcx.sess.source_map();
         let body_span = self.body_span;
@@ -145,6 +170,7 @@ impl<'a, 'tcx> Instrumentor<'a, 'tcx> {
                     counter_operand,
                     Op::Add,
                     ExpressionOperandId::ZERO,
+                    || Some(format!("{:?}", bcb)),
                 );
                 debug!(
                     "Injecting counter expression {:?} at: {:?}:\n{}\n==========",
@@ -152,11 +178,12 @@ impl<'a, 'tcx> Instrumentor<'a, 'tcx> {
                     span,
                     source_map.span_to_snippet(span).expect("Error getting source for span"),
                 );
+                graphviz_data.add_bcb_coverage_span_with_counter(bcb, &covspan, &expression);
                 let bb = self.basic_coverage_blocks[bcb].leader_bb();
                 let code_region = make_code_region(file_name, &source_file, span, body_span);
                 inject_statement(self.mir_body, expression, bb, Some(code_region));
             } else {
-                let counter = self.coverage_counters.make_counter();
+                let counter = self.coverage_counters.make_counter(|| Some(format!("{:?}", bcb)));
                 debug!(
                     "Injecting counter {:?} at: {:?}:\n{}\n==========",
                     counter,
@@ -165,6 +192,7 @@ impl<'a, 'tcx> Instrumentor<'a, 'tcx> {
                 );
                 let counter_operand = counter.as_operand_id();
                 bcb_counters[bcb] = Some(counter_operand);
+                graphviz_data.add_bcb_coverage_span_with_counter(bcb, &covspan, &counter);
                 let bb = self.basic_coverage_blocks[bcb].leader_bb();
                 let code_region = make_code_region(file_name, &source_file, span, body_span);
                 inject_statement(self.mir_body, counter, bb, Some(code_region));
diff --git a/compiler/rustc_mir/src/transform/coverage/spans.rs b/compiler/rustc_mir/src/transform/coverage/spans.rs
index 23531ecf229..61b5b148053 100644
--- a/compiler/rustc_mir/src/transform/coverage/spans.rs
+++ b/compiler/rustc_mir/src/transform/coverage/spans.rs
@@ -134,6 +134,14 @@ impl CoverageSpan {
         self.bcb == other.bcb
     }
 
+    pub fn format(&self, tcx: TyCtxt<'tcx>, mir_body: &'a mir::Body<'tcx>) -> String {
+        format!(
+            "{}\n    {}",
+            source_range_no_file(tcx, &self.span),
+            self.format_coverage_statements(tcx, mir_body).replace("\n", "\n    "),
+        )
+    }
+
     pub fn format_coverage_statements(
         &self,
         tcx: TyCtxt<'tcx>,
diff --git a/compiler/rustc_mir/src/util/generic_graphviz.rs b/compiler/rustc_mir/src/util/generic_graphviz.rs
new file mode 100644
index 00000000000..91499bb61c2
--- /dev/null
+++ b/compiler/rustc_mir/src/util/generic_graphviz.rs
@@ -0,0 +1,185 @@
+use rustc_data_structures::graph::{self, iterate};
+use rustc_graphviz as dot;
+use rustc_middle::ty::TyCtxt;
+use std::io::{self, Write};
+
+pub struct GraphvizWriter<
+    'a,
+    G: graph::DirectedGraph + graph::WithSuccessors + graph::WithStartNode + graph::WithNumNodes,
+    NodeContentFn: Fn(<G as rustc_data_structures::graph::DirectedGraph>::Node) -> Vec<String>,
+    EdgeLabelsFn: Fn(<G as rustc_data_structures::graph::DirectedGraph>::Node) -> Vec<String>,
+> {
+    graph: &'a G,
+    is_subgraph: bool,
+    graphviz_name: String,
+    graph_label: Option<String>,
+    node_content_fn: NodeContentFn,
+    edge_labels_fn: EdgeLabelsFn,
+}
+
+impl<
+    'a,
+    G: graph::DirectedGraph + graph::WithSuccessors + graph::WithStartNode + graph::WithNumNodes,
+    NodeContentFn: Fn(<G as rustc_data_structures::graph::DirectedGraph>::Node) -> Vec<String>,
+    EdgeLabelsFn: Fn(<G as rustc_data_structures::graph::DirectedGraph>::Node) -> Vec<String>,
+> GraphvizWriter<'a, G, NodeContentFn, EdgeLabelsFn>
+{
+    pub fn new(
+        graph: &'a G,
+        graphviz_name: &str,
+        node_content_fn: NodeContentFn,
+        edge_labels_fn: EdgeLabelsFn,
+    ) -> Self {
+        Self {
+            graph,
+            is_subgraph: false,
+            graphviz_name: graphviz_name.to_owned(),
+            graph_label: None,
+            node_content_fn,
+            edge_labels_fn,
+        }
+    }
+
+    pub fn new_subgraph(
+        graph: &'a G,
+        graphviz_name: &str,
+        node_content_fn: NodeContentFn,
+        edge_labels_fn: EdgeLabelsFn,
+    ) -> Self {
+        Self {
+            graph,
+            is_subgraph: true,
+            graphviz_name: graphviz_name.to_owned(),
+            graph_label: None,
+            node_content_fn,
+            edge_labels_fn,
+        }
+    }
+
+    pub fn set_graph_label(&mut self, graph_label: &str) {
+        self.graph_label = Some(graph_label.to_owned());
+    }
+
+    /// Write a graphviz DOT of the graph
+    pub fn write_graphviz<'tcx, W>(&self, tcx: TyCtxt<'tcx>, w: &mut W) -> io::Result<()>
+    where
+        W: Write,
+    {
+        let kind = if self.is_subgraph { "subgraph" } else { "digraph" };
+        let cluster = if self.is_subgraph { "cluster_" } else { "" }; // Print border around graph
+        // FIXME(richkadel): If/when migrating the MIR graphviz to this generic implementation,
+        // prepend "Mir_" to the graphviz_safe_def_name(def_id)
+        writeln!(w, "{} {}{} {{", kind, cluster, self.graphviz_name)?;
+
+        // Global graph properties
+        let font = format!(r#"fontname="{}""#, tcx.sess.opts.debugging_opts.graphviz_font);
+        let mut graph_attrs = vec![&font[..]];
+        let mut content_attrs = vec![&font[..]];
+
+        let dark_mode = tcx.sess.opts.debugging_opts.graphviz_dark_mode;
+        if dark_mode {
+            graph_attrs.push(r#"bgcolor="black""#);
+            graph_attrs.push(r#"fontcolor="white""#);
+            content_attrs.push(r#"color="white""#);
+            content_attrs.push(r#"fontcolor="white""#);
+        }
+
+        writeln!(w, r#"    graph [{}];"#, graph_attrs.join(" "))?;
+        let content_attrs_str = content_attrs.join(" ");
+        writeln!(w, r#"    node [{}];"#, content_attrs_str)?;
+        writeln!(w, r#"    edge [{}];"#, content_attrs_str)?;
+
+        // Graph label
+        if let Some(graph_label) = &self.graph_label {
+            self.write_graph_label(graph_label, w)?;
+        }
+
+        // Nodes
+        for node in iterate::post_order_from(self.graph, self.graph.start_node()) {
+            self.write_node(node, dark_mode, w)?;
+        }
+
+        // Edges
+        for source in iterate::post_order_from(self.graph, self.graph.start_node()) {
+            self.write_edges(source, w)?;
+        }
+        writeln!(w, "}}")
+    }
+
+    /// Write a graphviz DOT node for the given node.
+    pub fn write_node<W>(&self, node: G::Node, dark_mode: bool, w: &mut W) -> io::Result<()>
+    where
+        W: Write,
+    {
+        // Start a new node with the label to follow, in one of DOT's pseudo-HTML tables.
+        write!(w, r#"    {} [shape="none", label=<"#, self.node(node))?;
+
+        write!(w, r#"<table border="0" cellborder="1" cellspacing="0">"#)?;
+
+        // FIXME(richkadel): Need generic way to know if node header should have a different color
+        // let (blk, bgcolor) = if data.is_cleanup {
+        //    (format!("{:?} (cleanup)", node), "lightblue")
+        // } else {
+        //     let color = if dark_mode { "dimgray" } else { "gray" };
+        //     (format!("{:?}", node), color)
+        // };
+        let color = if dark_mode { "dimgray" } else { "gray" };
+        let (blk, bgcolor) = (format!("{:?}", node), color);
+        write!(
+            w,
+            r#"<tr><td bgcolor="{bgcolor}" {attrs} colspan="{colspan}">{blk}</td></tr>"#,
+            attrs = r#"align="center""#,
+            colspan = 1,
+            blk = blk,
+            bgcolor = bgcolor
+        )?;
+
+        for section in (self.node_content_fn)(node) {
+            write!(
+                w,
+                r#"<tr><td align="left" balign="left">{}</td></tr>"#,
+                dot::escape_html(&section).replace("\n", "<br/>")
+            )?;
+        }
+
+        // Close the table
+        write!(w, "</table>")?;
+
+        // Close the node label and the node itself.
+        writeln!(w, ">];")
+    }
+
+    /// Write graphviz DOT edges with labels between the given node and all of its successors.
+    fn write_edges<W>(&self, source: G::Node, w: &mut W) -> io::Result<()>
+    where
+        W: Write,
+    {
+        let edge_labels = (self.edge_labels_fn)(source);
+        for (index, target) in self.graph.successors(source).enumerate() {
+            let src = self.node(source);
+            let trg = self.node(target);
+            let escaped_edge_label = if let Some(edge_label) = edge_labels.get(index) {
+                dot::escape_html(edge_label).replace("\n", r#"<br align="left"/>"#)
+            } else {
+                "".to_owned()
+            };
+            writeln!(w, r#"    {} -> {} [label=<{}>];"#, src, trg, escaped_edge_label)?;
+        }
+        Ok(())
+    }
+
+    /// Write the graphviz DOT label for the overall graph. This is essentially a block of text that
+    /// will appear below the graph.
+    fn write_graph_label<W>(&self, label: &str, w: &mut W) -> io::Result<()>
+    where
+        W: Write,
+    {
+        let lines = label.split("\n").map(|s| dot::escape_html(s)).collect::<Vec<_>>();
+        let escaped_label = lines.join(r#"<br align="left"/>"#);
+        writeln!(w, r#"    label=<<br/><br/>{}<br align="left"/><br/><br/><br/>>;"#, escaped_label)
+    }
+
+    fn node(&self, node: G::Node) -> String {
+        format!("{:?}__{}", node, self.graphviz_name)
+    }
+}
diff --git a/compiler/rustc_mir/src/util/mod.rs b/compiler/rustc_mir/src/util/mod.rs
index 7da2f4ffe08..aaee0bc526d 100644
--- a/compiler/rustc_mir/src/util/mod.rs
+++ b/compiler/rustc_mir/src/util/mod.rs
@@ -7,6 +7,7 @@ pub mod storage;
 mod alignment;
 pub mod collect_writes;
 mod find_self_call;
+pub(crate) mod generic_graphviz;
 mod graphviz;
 pub(crate) mod pretty;
 pub(crate) mod spanview;
diff --git a/src/test/mir-opt/coverage_graphviz.bar.InstrumentCoverage.0.dot b/src/test/mir-opt/coverage_graphviz.bar.InstrumentCoverage.0.dot
new file mode 100644
index 00000000000..df9f7aa627b
--- /dev/null
+++ b/src/test/mir-opt/coverage_graphviz.bar.InstrumentCoverage.0.dot
@@ -0,0 +1,6 @@
+digraph Cov_0_4 {
+    graph [fontname="Courier, monospace"];
+    node [fontname="Courier, monospace"];
+    edge [fontname="Courier, monospace"];
+    bcb0__Cov_0_4 [shape="none", label=<<table border="0" cellborder="1" cellspacing="0"><tr><td bgcolor="gray" align="center" colspan="1">bcb0</td></tr><tr><td align="left" balign="left">Counter(bcb0) at 19:5-20:2<br/>    19:5-19:9: @0[0]: _0 = const true<br/>    20:2-20:2: @0.Return: return</td></tr><tr><td align="left" balign="left">bb0: Return</td></tr></table>>];
+}
diff --git a/src/test/mir-opt/coverage_graphviz.main.InstrumentCoverage.0.dot b/src/test/mir-opt/coverage_graphviz.main.InstrumentCoverage.0.dot
new file mode 100644
index 00000000000..051ef498fb7
--- /dev/null
+++ b/src/test/mir-opt/coverage_graphviz.main.InstrumentCoverage.0.dot
@@ -0,0 +1,11 @@
+digraph Cov_0_3 {
+    graph [fontname="Courier, monospace"];
+    node [fontname="Courier, monospace"];
+    edge [fontname="Courier, monospace"];
+    bcb2__Cov_0_3 [shape="none", label=<<table border="0" cellborder="1" cellspacing="0"><tr><td bgcolor="gray" align="center" colspan="1">bcb2</td></tr><tr><td align="left" balign="left">Counter(bcb2) at 14:6-14:6<br/>    14:6-14:6: @4.Goto: goto -&gt; bb0</td></tr><tr><td align="left" balign="left">bb4: Goto</td></tr></table>>];
+    bcb1__Cov_0_3 [shape="none", label=<<table border="0" cellborder="1" cellspacing="0"><tr><td bgcolor="gray" align="center" colspan="1">bcb1</td></tr><tr><td align="left" balign="left">Counter(bcb1) at 12:13-12:18<br/>    12:13-12:18: @5[0]: _0 = const ()<br/>Expression(bcb1 + 0) at 15:2-15:2<br/>    15:2-15:2: @5.Return: return</td></tr><tr><td align="left" balign="left">bb3: FalseEdge</td></tr><tr><td align="left" balign="left">bb5: Return</td></tr></table>>];
+    bcb0__Cov_0_3 [shape="none", label=<<table border="0" cellborder="1" cellspacing="0"><tr><td bgcolor="gray" align="center" colspan="1">bcb0</td></tr><tr><td align="left" balign="left">Counter(bcb0) at 11:12-11:17<br/>    11:12-11:17: @1.Call: _2 = bar() -&gt; [return: bb2, unwind: bb6]<br/>    11:12-11:17: @2[0]: FakeRead(ForMatchedPlace, _2)</td></tr><tr><td align="left" balign="left">bb0: FalseUnwind<br/>bb1: Call</td></tr><tr><td align="left" balign="left">bb2: SwitchInt</td></tr></table>>];
+    bcb2__Cov_0_3 -> bcb0__Cov_0_3 [label=<>];
+    bcb0__Cov_0_3 -> bcb2__Cov_0_3 [label=<false>];
+    bcb0__Cov_0_3 -> bcb1__Cov_0_3 [label=<otherwise>];
+}
diff --git a/src/test/mir-opt/coverage_graphviz.rs b/src/test/mir-opt/coverage_graphviz.rs
new file mode 100644
index 00000000000..b3c90c52837
--- /dev/null
+++ b/src/test/mir-opt/coverage_graphviz.rs
@@ -0,0 +1,20 @@
+// Test that `-Z instrument-coverage` with `-Z dump-mir-graphviz` generates a graphviz (.dot file)
+// rendering of the `BasicCoverageBlock` coverage control flow graph, with counters and
+// expressions.
+
+// needs-profiler-support
+// compile-flags: -Z instrument-coverage -Z dump-mir-graphviz
+// EMIT_MIR coverage_graphviz.main.InstrumentCoverage.0.dot
+// EMIT_MIR coverage_graphviz.bar.InstrumentCoverage.0.dot
+fn main() {
+    loop {
+        if bar() {
+            break;
+        }
+    }
+}
+
+#[inline(never)]
+fn bar() -> bool {
+    true
+}