From 3d41197d6803183cdcd88894d568a29215fd2afb Mon Sep 17 00:00:00 2001 From: Guray Ozen Date: Thu, 21 Aug 2025 16:02:31 +0200 Subject: [PATCH] [MLIR] Introduce RemarkEngine + pluggable remark streaming (YAML/Bitstream) (#152474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements structured, tooling-friendly optimization remarks with zero cost unless enabled. It implements: - `RemarkEngine` collects finalized remarks within `MLIRContext`. - `MLIRRemarkStreamerBase` abstract class streams them to a backend. - Backends: `MLIRLLVMRemarkStreamer` (bridges to llvm::remarks → YAML/Bitstream) or your own custom streamer. - Optional mirroring to DiagnosticEngine (printAsEmitRemarks + categories). - Off by default; no behavior change unless enabled. Thread-safe; ordering best-effort. ## Overview ``` Passes (reportOptimization*) │ ▼ +-------------------+ | RemarkEngine | collects +-------------------+ │ │ │ mirror │ stream ▼ ▼ emitRemark MLIRRemarkStreamerBase (abstract) │ ├── MLIRLLVMRemarkStreamer → llvm::remarks → YAML | Bitstream └── CustomStreamer → your sink ``` ## Enable Remark engine and Plug LLVM's Remark streamer ``` // Enable once per MLIRContext. This uses `MLIRLLVMRemarkStreamer` mlir::remark::enableOptimizationRemarksToFile( ctx, path, llvm::remarks::Format::YAML, cats); ``` ## API to emit remark ``` // Emit from a pass remark::passed(loc, categoryVectorizer, myPassname1) << "vectorized loop"; remark::missed(loc, categoryUnroll, "MyPass") << remark::reason("not profitable at this size") // Creates structured reason arg << remark::suggest("increase unroll factor to >=4"); // Creates structured suggestion arg remark::passed(loc, categoryVectorizer, myPassname1) << "vectorized loop" << remark::metric("tripCount", 128); // Create structured metric on-the-fly ``` --- mlir/docs/Remarks.md | 259 +++++++++++ mlir/include/mlir/IR/MLIRContext.h | 10 + mlir/include/mlir/IR/Remarks.h | 520 ++++++++++++++++++++++ mlir/include/mlir/Remark/RemarkStreamer.h | 49 ++ mlir/lib/CMakeLists.txt | 1 + mlir/lib/IR/CMakeLists.txt | 1 + mlir/lib/IR/MLIRContext.cpp | 19 + mlir/lib/IR/Remarks.cpp | 279 ++++++++++++ mlir/lib/Remark/CMakeLists.txt | 14 + mlir/lib/Remark/RemarkStreamer.cpp | 69 +++ mlir/unittests/IR/CMakeLists.txt | 2 + mlir/unittests/IR/RemarkTest.cpp | 315 +++++++++++++ 12 files changed, 1538 insertions(+) create mode 100644 mlir/docs/Remarks.md create mode 100644 mlir/include/mlir/IR/Remarks.h create mode 100644 mlir/include/mlir/Remark/RemarkStreamer.h create mode 100644 mlir/lib/IR/Remarks.cpp create mode 100644 mlir/lib/Remark/CMakeLists.txt create mode 100644 mlir/lib/Remark/RemarkStreamer.cpp create mode 100644 mlir/unittests/IR/RemarkTest.cpp diff --git a/mlir/docs/Remarks.md b/mlir/docs/Remarks.md new file mode 100644 index 000000000000..ee2ea855b8a6 --- /dev/null +++ b/mlir/docs/Remarks.md @@ -0,0 +1,259 @@ +# Remark Infrastructure + +Remarks are **structured, human- and machine-readable notes** emitted by the +compiler to explain: + +- What was transformed +- What was missed +- Why it happened + +The **`RemarkEngine`** collects finalized remarks during compilation and sends +them to a pluggable **streamer**. By default, MLIR integrates with LLVM’s +[`llvm::remarks`](https://llvm.org/docs/Remarks.html), allowing you to: + +- Stream remarks as passes run +- Serialize them to **YAML** or **LLVM bitstream** for tooling + +*** + +## Key Points + +- **Opt-in** – Disabled by default; zero overhead unless enabled. +- **Per-context** – Configured on `MLIRContext`. +- **Formats** – LLVM Remark engine (YAML / Bitstream) or custom streamers. +- **Kinds** – `Passed`, `Missed`, `Failure`, `Analysis`. +- **API** – Lightweight streaming interface using `<<` (like MLIR diagnostics). + +*** + +## How It Works + +Two main components: + +- **`RemarkEngine`** (owned by `MLIRContext`): Receives finalized + `InFlightRemark`s, optionally mirrors them to the `DiagnosticEngine`, and + dispatches to the installed streamer. + +- **`MLIRRemarkStreamerBase`** (abstract): Backend interface with a single hook: + + ```c++ + virtual void streamOptimizationRemark(const Remark &remark) = 0; + ``` + +**Default backend – `MLIRLLVMRemarkStreamer`** Adapts `mlir::Remark` to LLVM’s +remark format and writes YAML/bitstream via `llvm::remarks::RemarkStreamer`. + +**Ownership flow:** `MLIRContext` → `RemarkEngine` → `MLIRRemarkStreamerBase` + +*** + +## Categories + +MLIR provides four built-in remark categories (extendable if needed): + +#### 1. **Passed** + +Optimization/transformation succeeded. + +``` +[Passed] RemarkName | Category:Vectorizer:myPass1 | Function=foo | Remark="vectorized loop", tripCount=128 +``` + +#### 2. **Missed** + +Optimization/transformation didn’t apply — ideally with actionable feedback. + +``` +[Missed] | Category:Unroll | Function=foo | Reason="tripCount=4 < threshold=256", Suggestion="increase unroll to 128" +``` + +#### 3. **Failure** + +Optimization/transformation attempted but failed. This is slightly different +from the `Missed` category. + +For example, the user specifies `-use-max-register=100` when invoking the +compiler, but the attempt fails for some reason: + +```bash +$ your-compiler -use-max-register=100 mycode.xyz +``` + +``` +[Failed] Category:RegisterAllocator | Reason="Limiting to use-max-register=100 failed; it now uses 104 registers for better performance" +``` + +#### 4. **Analysis** + +Neutral analysis results. + +``` +[Analysis] Category:Register | Remark="Kernel uses 168 registers" +[Analysis] Category:Register | Remark="Kernel uses 10kB local memory" +``` + +*** + +## Emitting Remarks + +The `remark::*` helpers return an **in-flight remark**. +You append strings or key–value metrics using `<<`. + +### Remark Options + +When constructing a remark, you typically provide four fields that are `StringRef`: + +1. **Remark name** – identifiable name +2. **Category** – high-level classification +3. **Sub-category** – more fine-grained classification +4. **Function name** – the function where the remark originates + + +### Example + +```c++ +#include "mlir/IR/Remarks.h" + +LogicalResult MyPass::runOnOperation() { + Location loc = getOperation()->getLoc(); + + remark::RemarkOpts opts = remark::RemarkOpts::name(MyRemarkName1) + .category(categoryVectorizer) + .function(fName) + .subCategory(myPassname1); + + // PASSED + remark::passed(loc, opts) + << "vectorized loop" + << remark::metric("tripCount", 128); + + // ANALYSIS + remark::analysis(loc, opts) + << "Kernel uses 168 registers"; + + // MISSED (with reason + suggestion) + int tripBad = 4, threshold = 256, target = 128; + remark::missed(loc, opts) + << remark::reason("tripCount={0} < threshold={1}", tripBad, threshold) + << remark::suggest("increase unroll to {0}", target); + + // FAILURE + remark::failed(loc, opts) + << remark::reason("failed due to unsupported pattern"); + + return success(); +} +``` + +*** + +### Metrics and Shortcuts + +Helper functions accept +[LLVM format](https://llvm.org/docs/ProgrammersManual.html#formatting-strings-the-formatv-function) +style strings. This format builds lazily, so remarks are zero-cost when +disabled. + +#### Adding Remarks + +- **`remark::add(fmt, ...)`** – Shortcut for `metric("Remark", ...)`. + +#### Adding Reasons + +- **`remark::reason(fmt, ...)`** – Shortcut for `metric("Reason", ...)`. Used to + explain why a remark was missed or failed. + +#### Adding Suggestions + +- **`remark::suggest(fmt, ...)`** – Shortcut for `metric("Suggestion", ...)`. + Used to provide actionable feedback. + +#### Adding Custom Metrics + +- **`remark::metric(key, value)`** – Adds a structured key–value metric. + +Example: tracking `TripCount`. When exported to YAML, it appears under `args` +for machine readability: + +```cpp +remark::metric("TripCount", value) +``` + +#### String Metrics + +Passing a plain string (e.g. `<< "vectorized loop"`) is equivalent to: + +```cpp +metric("Remark", "vectorized loop") +``` + +*** + +## Enabling Remarks + +### 1. **With LLVMRemarkStreamer (YAML or Bitstream)** + +Persists remarks to a file in the chosen format. + +```c++ +mlir::remark::RemarkCategories cats{/*passed=*/categoryLoopunroll, + /*missed=*/std::nullopt, + /*analysis=*/std::nullopt, + /*failed=*/categoryLoopunroll}; + +mlir::remark::enableOptimizationRemarksWithLLVMStreamer( + context, yamlFile, llvm::remarks::Format::YAML, cats); +``` + +**YAML format** – human-readable, easy to diff: + +```yaml +--- !Passed +pass: Category:SubCategory +name: MyRemarkName1 +function: myFunc +loc: myfile.mlir:12:3 +args: + - Remark: vectorized loop + - tripCount: 128 +``` + +**Bitstream format** – compact binary for large runs. + +*** + +### 2. **With `mlir::emitRemarks` (No Streamer)** + +If the streamer isn't passed, the remarks are mirrored to the `DiagnosticEngine` +using `mlir::emitRemarks` + +```c++ +mlir::remark::RemarkCategories cats{/*passed=*/categoryLoopunroll, + /*missed=*/std::nullopt, + /*analysis=*/std::nullopt, + /*failed=*/categoryLoopunroll}; +remark::enableOptimizationRemarks( + /*streamer=*/nullptr, cats, + /*printAsEmitRemarks=*/true); +``` + +*** + +### 3. **With a Custom Streamer** + +You can implement a custom streamer by inheriting `MLIRRemarkStreamerBase` to +consume remarks in any format. + +```c++ +class MyStreamer : public MLIRRemarkStreamerBase { +public: + void streamOptimizationRemark(const Remark &remark) override { + // Convert and write remark to your custom format + } +}; + +auto myStreamer = std::make_unique(); +remark::enableOptimizationRemarks( + /*streamer=*/myStreamer, cats, + /*printAsEmitRemarks=*/true); +``` diff --git a/mlir/include/mlir/IR/MLIRContext.h b/mlir/include/mlir/IR/MLIRContext.h index ef8dab87f131..969002925647 100644 --- a/mlir/include/mlir/IR/MLIRContext.h +++ b/mlir/include/mlir/IR/MLIRContext.h @@ -34,6 +34,9 @@ class MLIRContextImpl; class RegisteredOperationName; class StorageUniquer; class IRUnit; +namespace remark::detail { +class RemarkEngine; +} // namespace remark::detail /// MLIRContext is the top-level object for a collection of MLIR operations. It /// holds immortal uniqued objects like types, and the tables used to unique @@ -212,6 +215,13 @@ public: /// Returns the diagnostic engine for this context. DiagnosticEngine &getDiagEngine(); + /// Returns the remark engine for this context, or nullptr if none has been + /// set. + remark::detail::RemarkEngine *getRemarkEngine(); + + /// Set the remark engine for this context. + void setRemarkEngine(std::unique_ptr engine); + /// Returns the storage uniquer used for creating affine constructs. StorageUniquer &getAffineUniquer(); diff --git a/mlir/include/mlir/IR/Remarks.h b/mlir/include/mlir/IR/Remarks.h new file mode 100644 index 000000000000..a737babc6611 --- /dev/null +++ b/mlir/include/mlir/IR/Remarks.h @@ -0,0 +1,520 @@ +//===- Remarks.h - MLIR Optimization Remark ----------------------*- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file defines utilities for emitting optimization remarks. +// +//===----------------------------------------------------------------------===// + +#ifndef MLIR_IR_REMARKS_H +#define MLIR_IR_REMARKS_H + +#include "llvm/ADT/StringExtras.h" +#include "llvm/IR/DiagnosticInfo.h" +#include "llvm/Remarks/Remark.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/Regex.h" +#include + +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Value.h" + +namespace mlir::remark { + +/// Define an the set of categories to accept. By default none are, the provided +/// regex matches against the category names for each kind of remark. +struct RemarkCategories { + std::optional passed, missed, analysis, failed; +}; + +/// Categories describe the outcome of an transformation, not the mechanics of +/// emitting/serializing remarks. +enum class RemarkKind { + RemarkUnknown = 0, + + /// An optimization was applied. + RemarkPassed, + + /// A profitable optimization opportunity was found but not applied. + RemarkMissed, + + /// The compiler attempted the optimization but failed (e.g., legality + /// checks, or better opportunites). + RemarkFailure, + + /// Informational context (e.g., analysis numbers) without a pass/fail + /// outcome. + RemarkAnalysis, +}; + +using namespace llvm; + +/// Options to create a Remark +struct RemarkOpts { + StringRef remarkName; // Identifiable name + StringRef categoryName; // Category name (subject to regex filtering) + StringRef subCategoryName; // Subcategory name + StringRef functionName; // Function name if available + RemarkOpts() = delete; + // Construct RemarkOpts from a remark name. + static constexpr RemarkOpts name(StringRef n) { + return RemarkOpts{n, {}, {}, {}}; + } + /// Return a copy with the category set. + constexpr RemarkOpts category(StringRef v) const { + return {remarkName, v, subCategoryName, functionName}; + } + /// Return a copy with the subcategory set. + constexpr RemarkOpts subCategory(StringRef v) const { + return {remarkName, categoryName, v, functionName}; + } + /// Return a copy with the function name set. + constexpr RemarkOpts function(StringRef v) const { + return {remarkName, categoryName, subCategoryName, v}; + } +}; + +} // namespace mlir::remark + +namespace mlir::remark::detail { +//===----------------------------------------------------------------------===// +// Remark Base Class +//===----------------------------------------------------------------------===// +class Remark { + +public: + Remark(RemarkKind remarkKind, DiagnosticSeverity severity, Location loc, + RemarkOpts opts) + : remarkKind(remarkKind), functionName(opts.functionName), loc(loc), + categoryName(opts.categoryName), subCategoryName(opts.subCategoryName), + remarkName(opts.remarkName) { + if (!categoryName.empty() && !subCategoryName.empty()) { + (llvm::Twine(categoryName) + ":" + subCategoryName) + .toStringRef(fullCategoryName); + } + } + + // Remark argument that is a key-value pair that can be printed as machine + // parsable args. + struct Arg { + std::string key; + std::string val; + Arg(llvm::StringRef m) : key("Remark"), val(m) {} + Arg(llvm::StringRef k, llvm::StringRef v) : key(k), val(v) {} + Arg(llvm::StringRef k, std::string v) : key(k), val(std::move(v)) {} + Arg(llvm::StringRef k, const char *v) : Arg(k, llvm::StringRef(v)) {} + Arg(llvm::StringRef k, Value v); + Arg(llvm::StringRef k, Type t); + Arg(llvm::StringRef k, bool b) : key(k), val(b ? "true" : "false") {} + + // One constructor for all arithmetic types except bool. + template && + !std::is_same_v>> + Arg(llvm::StringRef k, T v) : key(k) { + if constexpr (std::is_floating_point_v) { + llvm::raw_string_ostream os(val); + os << v; + } else if constexpr (std::is_signed_v) { + val = llvm::itostr(static_cast(v)); + } else { + val = llvm::utostr(static_cast(v)); + } + } + }; + + void insert(llvm::StringRef s); + void insert(Arg a); + + void print(llvm::raw_ostream &os, bool printLocation = false) const; + + Location getLocation() const { return loc; } + /// Diagnostic -> Remark + llvm::remarks::Remark generateRemark() const; + + StringRef getFunction() const { + if (!functionName.empty()) + return functionName; + return ""; + } + + llvm::StringRef getCategoryName() const { return categoryName; } + + llvm::StringRef getFullCategoryName() const { + if (categoryName.empty() && subCategoryName.empty()) + return {}; + if (subCategoryName.empty()) + return categoryName; + if (categoryName.empty()) + return subCategoryName; + return fullCategoryName; + } + + StringRef getRemarkName() const { + if (remarkName.empty()) + return ""; + return remarkName; + } + + std::string getMsg() const; + + ArrayRef getArgs() const { return args; } + + llvm::remarks::Type getRemarkType() const; + + StringRef getRemarkTypeString() const; + +protected: + /// Keeps the MLIR diagnostic kind, which is used to determine the + /// diagnostic kind in the LLVM remark streamer. + RemarkKind remarkKind; + /// Name of the convering function like interface + StringRef functionName; + + Location loc; + /// Sub category passname e.g., "Unroll" or "UnrollAndJam" + StringRef categoryName; + + /// Sub category name "Loop Optimizer" + StringRef subCategoryName; + + /// Combined name for category and sub-category + SmallString<64> fullCategoryName; + + /// Remark identifier + StringRef remarkName; + + /// Args collected via the streaming interface. + SmallVector args; + +private: + /// Convert the MLIR diagnostic severity to LLVM diagnostic severity. + static llvm::DiagnosticSeverity + makeLLVMSeverity(DiagnosticSeverity severity) { + switch (severity) { + case DiagnosticSeverity::Note: + return llvm::DiagnosticSeverity::DS_Note; + case DiagnosticSeverity::Warning: + return llvm::DiagnosticSeverity::DS_Warning; + case DiagnosticSeverity::Error: + return llvm::DiagnosticSeverity::DS_Error; + case DiagnosticSeverity::Remark: + return llvm::DiagnosticSeverity::DS_Remark; + } + llvm_unreachable("Unknown diagnostic severity"); + } + /// Convert the MLIR remark kind to LLVM diagnostic kind. + static llvm::DiagnosticKind makeLLVMKind(RemarkKind remarkKind) { + switch (remarkKind) { + case RemarkKind::RemarkUnknown: + return llvm::DiagnosticKind::DK_Generic; + case RemarkKind::RemarkPassed: + return llvm::DiagnosticKind::DK_OptimizationRemark; + case RemarkKind::RemarkMissed: + return llvm::DiagnosticKind::DK_OptimizationRemarkMissed; + case RemarkKind::RemarkFailure: + return llvm::DiagnosticKind::DK_OptimizationFailure; + case RemarkKind::RemarkAnalysis: + return llvm::DiagnosticKind::DK_OptimizationRemarkAnalysis; + } + llvm_unreachable("Unknown diagnostic kind"); + } +}; + +inline Remark &operator<<(Remark &r, StringRef s) { + r.insert(s); + return r; +} +inline Remark &&operator<<(Remark &&r, StringRef s) { + r.insert(s); + return std::move(r); +} +inline Remark &operator<<(Remark &r, const Remark::Arg &kv) { + r.insert(kv); + return r; +} + +//===----------------------------------------------------------------------===// +// Shorthand aliases for different kinds of remarks. +//===----------------------------------------------------------------------===// + +template +class OptRemarkBase final : public Remark { +public: + explicit OptRemarkBase(Location loc, RemarkOpts opts) + : Remark(K, S, loc, opts) {} +}; + +using OptRemarkAnalysis = + OptRemarkBase; + +using OptRemarkPass = + OptRemarkBase; + +using OptRemarkMissed = + OptRemarkBase; + +using OptRemarkFailure = + OptRemarkBase; + +class RemarkEngine; + +//===----------------------------------------------------------------------===// +// InFlightRemark +//===----------------------------------------------------------------------===// + +/// Lazy text building for zero cost string formatting. +struct LazyTextBuild { + llvm::StringRef key; + std::function thunk; +}; + +/// InFlightRemark is a RAII class that holds a reference to a Remark +/// instance and allows to build the remark using the << operator. The remark +/// is emitted when the InFlightRemark instance is destroyed, which happens +/// when the scope ends or when the InFlightRemark instance is moved. +/// Similar to InFlightDiagnostic, but for remarks. +class InFlightRemark { +public: + explicit InFlightRemark(std::unique_ptr diag) + : remark(std::move(diag)) {} + + InFlightRemark(RemarkEngine &eng, std::unique_ptr diag) + : owner(&eng), remark(std::move(diag)) {} + + InFlightRemark() = default; // empty ctor + + InFlightRemark &operator<<(const LazyTextBuild &l) { + if (remark) + *remark << Remark::Arg(l.key, l.thunk()); + return *this; + } + + // Generic path, but *not* for Lazy + template , LazyTextBuild>>> + InFlightRemark &operator<<(T &&arg) { + if (remark) + *remark << std::forward(arg); + return *this; + } + + explicit operator bool() const { return remark != nullptr; } + + ~InFlightRemark(); + + InFlightRemark(const InFlightRemark &) = delete; + InFlightRemark &operator=(const InFlightRemark &) = delete; + InFlightRemark(InFlightRemark &&) = default; + InFlightRemark &operator=(InFlightRemark &&) = default; + +private: + RemarkEngine *owner{nullptr}; + std::unique_ptr remark; +}; + +//===----------------------------------------------------------------------===// +// MLIR Remark Streamer +//===----------------------------------------------------------------------===// + +/// Base class for MLIR remark streamers that is used to stream +/// optimization remarks to the underlying remark streamer. The derived classes +/// should implement the `streamOptimizationRemark` method to provide the +/// actual streaming implementation. +class MLIRRemarkStreamerBase { +public: + virtual ~MLIRRemarkStreamerBase() = default; + /// Stream an optimization remark to the underlying remark streamer. It is + /// called by the RemarkEngine to stream the optimization remarks. + /// + /// It must be overridden by the derived classes to provide + /// the actual streaming implementation. + virtual void streamOptimizationRemark(const Remark &remark) = 0; + + virtual void finalize() {} // optional +}; + +//===----------------------------------------------------------------------===// +// Remark Engine (MLIR Context will own this class) +//===----------------------------------------------------------------------===// + +class RemarkEngine { +private: + /// Regex that filters missed optimization remarks: only matching one are + /// reported. + std::optional missFilter; + /// The category for passed optimization remarks. + std::optional passedFilter; + /// The category for analysis remarks. + std::optional analysisFilter; + /// The category for failed optimization remarks. + std::optional failedFilter; + /// The MLIR remark streamer that will be used to emit the remarks. + std::unique_ptr remarkStreamer; + /// When is enabled, engine also prints remarks as mlir::emitRemarks. + bool printAsEmitRemarks = false; + + /// Return true if missed optimization remarks are enabled, override + /// to provide different implementation. + bool isMissedOptRemarkEnabled(StringRef categoryName) const; + + /// Return true if passed optimization remarks are enabled, override + /// to provide different implementation. + bool isPassedOptRemarkEnabled(StringRef categoryName) const; + + /// Return true if analysis optimization remarks are enabled, override + /// to provide different implementation. + bool isAnalysisOptRemarkEnabled(StringRef categoryName) const; + + /// Return true if analysis optimization remarks are enabled, override + /// to provide different implementation. + bool isFailedOptRemarkEnabled(StringRef categoryName) const; + + /// Return true if any type of remarks are enabled for this pass. + bool isAnyRemarkEnabled(StringRef categoryName) const { + return isMissedOptRemarkEnabled(categoryName) || + isPassedOptRemarkEnabled(categoryName) || + isFailedOptRemarkEnabled(categoryName) || + isAnalysisOptRemarkEnabled(categoryName); + } + + /// Emit a remark using the given maker function, which should return + /// a Remark instance. The remark will be emitted using the main + /// remark streamer. + template + InFlightRemark makeRemark(Args &&...args); + + template + InFlightRemark emitIfEnabled(Location loc, RemarkOpts opts, + bool (RemarkEngine::*isEnabled)(StringRef) + const); + +public: + /// Default constructor is deleted, use the other constructor. + RemarkEngine() = delete; + + /// Constructs Remark engine with optional category names. If a category + /// name is not provided, it is not enabled. The category names are used to + /// filter the remarks that are emitted. + RemarkEngine(bool printAsEmitRemarks, const RemarkCategories &cats); + + /// Destructor that will close the output file and reset the + /// main remark streamer. + ~RemarkEngine(); + + /// Setup the remark engine with the given output path and format. + LogicalResult initialize(std::unique_ptr streamer, + std::string *errMsg); + + /// Report a remark. + void report(const Remark &&remark); + + /// Report a successful remark, this will create an InFlightRemark + /// that can be used to build the remark using the << operator. + InFlightRemark emitOptimizationRemark(Location loc, RemarkOpts opts); + + /// Report a missed optimization remark + /// that can be used to build the remark using the << operator. + InFlightRemark emitOptimizationRemarkMiss(Location loc, RemarkOpts opts); + + /// Report a failed optimization remark, this will create an InFlightRemark + /// that can be used to build the remark using the << operator. + InFlightRemark emitOptimizationRemarkFailure(Location loc, RemarkOpts opts); + + /// Report an analysis remark, this will create an InFlightRemark + /// that can be used to build the remark using the << operator. + InFlightRemark emitOptimizationRemarkAnalysis(Location loc, RemarkOpts opts); +}; + +template +inline InFlightRemark withEngine(Fn fn, Location loc, Args &&...args) { + MLIRContext *ctx = loc->getContext(); + + RemarkEngine *enginePtr = ctx->getRemarkEngine(); + + if (LLVM_UNLIKELY(enginePtr)) + return (enginePtr->*fn)(loc, std::forward(args)...); + + return {}; +} + +} // namespace mlir::remark::detail + +namespace mlir::remark { + +/// Create a Reason with llvm::formatv formatting. +template +inline detail::LazyTextBuild reason(const char *fmt, Ts &&...ts) { + return {"Reason", [=] { return llvm::formatv(fmt, ts...).str(); }}; +} + +/// Create a Suggestion with llvm::formatv formatting. +template +inline detail::LazyTextBuild suggest(const char *fmt, Ts &&...ts) { + return {"Suggestion", [=] { return llvm::formatv(fmt, ts...).str(); }}; +} + +/// Create a Remark with llvm::formatv formatting. +template +inline detail::LazyTextBuild add(const char *fmt, Ts &&...ts) { + return {"Remark", [=] { return llvm::formatv(fmt, ts...).str(); }}; +} + +template +inline detail::LazyTextBuild metric(StringRef key, V &&v) { + using DV = std::decay_t; + return {key, [key, vv = DV(std::forward(v))]() mutable { + // Reuse Arg's formatting logic and return just the value string. + return detail::Remark::Arg(key, std::move(vv)).val; + }}; +} +//===----------------------------------------------------------------------===// +// Emitters +//===----------------------------------------------------------------------===// + +/// Report an optimization remark that was passed. +inline detail::InFlightRemark passed(Location loc, RemarkOpts opts) { + return withEngine(&detail::RemarkEngine::emitOptimizationRemark, loc, opts); +} + +/// Report an optimization remark that was missed. +inline detail::InFlightRemark missed(Location loc, RemarkOpts opts) { + return withEngine(&detail::RemarkEngine::emitOptimizationRemarkMiss, loc, + opts); +} + +/// Report an optimization remark that failed. +inline detail::InFlightRemark failed(Location loc, RemarkOpts opts) { + return withEngine(&detail::RemarkEngine::emitOptimizationRemarkFailure, loc, + opts); +} + +/// Report an optimization analysis remark. +inline detail::InFlightRemark analysis(Location loc, RemarkOpts opts) { + return withEngine(&detail::RemarkEngine::emitOptimizationRemarkAnalysis, loc, + opts); +} + +//===----------------------------------------------------------------------===// +// Setup +//===----------------------------------------------------------------------===// + +/// Setup remarks for the context. This function will enable the remark engine +/// and set the streamer to be used for optimization remarks. The remark +/// categories are used to filter the remarks that will be emitted by the remark +/// engine. If a category is not specified, it will not be emitted. If +/// `printAsEmitRemarks` is true, the remarks will be printed as +/// mlir::emitRemarks. 'streamer' must inherit from MLIRRemarkStreamerBase and +/// will be used to stream the remarks. +LogicalResult enableOptimizationRemarks( + MLIRContext &ctx, + std::unique_ptr streamer, + const remark::RemarkCategories &cats, bool printAsEmitRemarks = false); + +} // namespace mlir::remark + +#endif // MLIR_IR_REMARKS_H diff --git a/mlir/include/mlir/Remark/RemarkStreamer.h b/mlir/include/mlir/Remark/RemarkStreamer.h new file mode 100644 index 000000000000..8bfd176d9bad --- /dev/null +++ b/mlir/include/mlir/Remark/RemarkStreamer.h @@ -0,0 +1,49 @@ +//===- RemarkStreamer.h - MLIR Optimization Remark ---------------*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file defines LLVMRemarkStreamer plugging class that uses LLVM's +// streamer. +// +//===----------------------------------------------------------------------===// + +#include "mlir/IR/Remarks.h" + +#include "llvm/Remarks/RemarkStreamer.h" +#include "llvm/Support/ToolOutputFile.h" + +namespace mlir::remark::detail { + +/// Concrete streamer that writes LLVM optimization remarks to a file +/// (YAML or Bitstream). Lives outside core. +class LLVMRemarkStreamer final : public MLIRRemarkStreamerBase { +public: + static FailureOr> + createToFile(llvm::StringRef path, llvm::remarks::Format fmt); + + void streamOptimizationRemark(const Remark &remark) override; + void finalize() override {} + ~LLVMRemarkStreamer() override; + +private: + LLVMRemarkStreamer() = default; + + std::unique_ptr remarkStreamer; + std::unique_ptr file; +}; +} // namespace mlir::remark::detail + +namespace mlir::remark { +/// Enable optimization remarks to a file with the given path and format. +/// The remark categories are used to filter the remarks that are emitted. +/// If the printAsEmitRemarks flag is set, remarks will also be printed using +/// mlir::emitRemarks. +LogicalResult enableOptimizationRemarksWithLLVMStreamer( + MLIRContext &ctx, StringRef filePath, llvm::remarks::Format fmt, + const RemarkCategories &cat, bool printAsEmitRemarks = false); + +} // namespace mlir::remark diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index 191b5ab6a6f5..91ed05f6548d 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(Parser) add_subdirectory(Pass) add_subdirectory(Query) add_subdirectory(Reducer) +add_subdirectory(Remark) add_subdirectory(Rewrite) add_subdirectory(Support) add_subdirectory(TableGen) diff --git a/mlir/lib/IR/CMakeLists.txt b/mlir/lib/IR/CMakeLists.txt index 3ef69cea18f0..d95bdc957e3c 100644 --- a/mlir/lib/IR/CMakeLists.txt +++ b/mlir/lib/IR/CMakeLists.txt @@ -33,6 +33,7 @@ add_mlir_library(MLIRIR PatternMatch.cpp Region.cpp RegionKindInterface.cpp + Remarks.cpp SymbolTable.cpp TensorEncoding.cpp Types.cpp diff --git a/mlir/lib/IR/MLIRContext.cpp b/mlir/lib/IR/MLIRContext.cpp index 8d78929c0073..1fa04ed8e738 100644 --- a/mlir/lib/IR/MLIRContext.cpp +++ b/mlir/lib/IR/MLIRContext.cpp @@ -25,6 +25,7 @@ #include "mlir/IR/Location.h" #include "mlir/IR/OpImplementation.h" #include "mlir/IR/OperationSupport.h" +#include "mlir/IR/Remarks.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/Twine.h" #include "llvm/Support/Allocator.h" @@ -133,6 +134,11 @@ public: //===--------------------------------------------------------------------===// DiagnosticEngine diagEngine; + //===--------------------------------------------------------------------===// + // Remark + //===--------------------------------------------------------------------===// + std::unique_ptr remarkEngine; + //===--------------------------------------------------------------------===// // Options //===--------------------------------------------------------------------===// @@ -387,6 +393,19 @@ bool MLIRContext::hasActionHandler() { return (bool)getImpl().actionHandler; } /// Returns the diagnostic engine for this context. DiagnosticEngine &MLIRContext::getDiagEngine() { return getImpl().diagEngine; } +//===----------------------------------------------------------------------===// +// Remark Handlers +//===----------------------------------------------------------------------===// + +void MLIRContext::setRemarkEngine( + std::unique_ptr engine) { + getImpl().remarkEngine = std::move(engine); +} + +remark::detail::RemarkEngine *MLIRContext::getRemarkEngine() { + return getImpl().remarkEngine.get(); +} + //===----------------------------------------------------------------------===// // Dialect and Operation Registration //===----------------------------------------------------------------------===// diff --git a/mlir/lib/IR/Remarks.cpp b/mlir/lib/IR/Remarks.cpp new file mode 100644 index 000000000000..78c964427868 --- /dev/null +++ b/mlir/lib/IR/Remarks.cpp @@ -0,0 +1,279 @@ +//===- Remarks.cpp - MLIR Remarks -----------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "mlir/IR/Remarks.h" + +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/Value.h" + +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" + +using namespace mlir::remark::detail; + +//------------------------------------------------------------------------------ +// Remark +//------------------------------------------------------------------------------ + +Remark::Arg::Arg(llvm::StringRef k, Value v) : key(k) { + llvm::raw_string_ostream os(val); + os << v; +} + +Remark::Arg::Arg(llvm::StringRef k, Type t) : key(k) { + llvm::raw_string_ostream os(val); + os << t; +} + +void Remark::insert(llvm::StringRef s) { args.emplace_back(s); } +void Remark::insert(Arg a) { args.push_back(std::move(a)); } + +// Simple helper to print key=val list (sorted). +static void printArgs(llvm::raw_ostream &os, llvm::ArrayRef args) { + if (args.empty()) + return; + + llvm::SmallVector sorted(args.begin(), args.end()); + llvm::sort(sorted, [](const Remark::Arg &a, const Remark::Arg &b) { + return a.key < b.key; + }); + + for (size_t i = 0; i < sorted.size(); ++i) { + const auto &a = sorted[i]; + os << a.key << "="; + + llvm::StringRef val(a.val); + bool needsQuote = val.contains(' ') || val.contains(',') || + val.contains('{') || val.contains('}'); + if (needsQuote) + os << '"' << val << '"'; + else + os << val; + + if (i + 1 < sorted.size()) + os << ", "; + } +} + +/// Print the remark to the given output stream. +/// Example output: +// clang-format off +/// [Missed] Category: Loop | Pass:Unroller | Function=main | Reason="tripCount=4 < threshold=256" +/// [Failure] LoopOptimizer | Reason="failed due to unsupported pattern" +// clang-format on +void Remark::print(llvm::raw_ostream &os, bool printLocation) const { + // Header: [Type] pass:remarkName + StringRef type = getRemarkTypeString(); + StringRef categoryName = getFullCategoryName(); + StringRef name = remarkName; + + os << '[' << type << "] "; + os << name << " | "; + if (!categoryName.empty()) + os << "Category:" << categoryName << " | "; + if (!functionName.empty()) + os << "Function=" << getFunction() << " | "; + + if (printLocation) { + if (auto flc = mlir::dyn_cast(getLocation())) + os << " @" << flc.getFilename() << ":" << flc.getLine() << ":" + << flc.getColumn(); + } + + printArgs(os, getArgs()); +} + +std::string Remark::getMsg() const { + std::string s; + llvm::raw_string_ostream os(s); + print(os); + os.flush(); + return s; +} + +llvm::StringRef Remark::getRemarkTypeString() const { + switch (remarkKind) { + case RemarkKind::RemarkUnknown: + return "Unknown"; + case RemarkKind::RemarkPassed: + return "Passed"; + case RemarkKind::RemarkMissed: + return "Missed"; + case RemarkKind::RemarkFailure: + return "Failure"; + case RemarkKind::RemarkAnalysis: + return "Analysis"; + } + llvm_unreachable("Unknown remark kind"); +} + +llvm::remarks::Type Remark::getRemarkType() const { + switch (remarkKind) { + case RemarkKind::RemarkUnknown: + return llvm::remarks::Type::Unknown; + case RemarkKind::RemarkPassed: + return llvm::remarks::Type::Passed; + case RemarkKind::RemarkMissed: + return llvm::remarks::Type::Missed; + case RemarkKind::RemarkFailure: + return llvm::remarks::Type::Failure; + case RemarkKind::RemarkAnalysis: + return llvm::remarks::Type::Analysis; + } + llvm_unreachable("Unknown remark kind"); +} + +llvm::remarks::Remark Remark::generateRemark() const { + auto locLambda = [&]() -> llvm::remarks::RemarkLocation { + if (auto flc = dyn_cast(getLocation())) + return {flc.getFilename(), flc.getLine(), flc.getColumn()}; + return {"", 0, 0}; + }; + + llvm::remarks::Remark r; // The result. + r.RemarkType = getRemarkType(); + r.RemarkName = getRemarkName(); + // MLIR does not use passes; instead, it has categories and sub-categories. + r.PassName = getFullCategoryName(); + r.FunctionName = getFunction(); + r.Loc = locLambda(); + for (const Remark::Arg &arg : getArgs()) { + r.Args.emplace_back(); + r.Args.back().Key = arg.key; + r.Args.back().Val = arg.val; + } + return r; +} + +//===----------------------------------------------------------------------===// +// InFlightRemark +//===----------------------------------------------------------------------===// + +InFlightRemark::~InFlightRemark() { + if (remark && owner) + owner->report(std::move(*remark)); + owner = nullptr; +} + +//===----------------------------------------------------------------------===// +// Remark Engine +//===----------------------------------------------------------------------===// + +template +InFlightRemark RemarkEngine::makeRemark(Args &&...args) { + static_assert(std::is_base_of_v, + "RemarkT must derive from Remark"); + return InFlightRemark(*this, + std::make_unique(std::forward(args)...)); +} + +template +InFlightRemark +RemarkEngine::emitIfEnabled(Location loc, RemarkOpts opts, + bool (RemarkEngine::*isEnabled)(StringRef) const) { + return (this->*isEnabled)(opts.categoryName) ? makeRemark(loc, opts) + : InFlightRemark{}; +} + +bool RemarkEngine::isMissedOptRemarkEnabled(StringRef categoryName) const { + return missFilter && missFilter->match(categoryName); +} + +bool RemarkEngine::isPassedOptRemarkEnabled(StringRef categoryName) const { + return passedFilter && passedFilter->match(categoryName); +} + +bool RemarkEngine::isAnalysisOptRemarkEnabled(StringRef categoryName) const { + return analysisFilter && analysisFilter->match(categoryName); +} + +bool RemarkEngine::isFailedOptRemarkEnabled(StringRef categoryName) const { + return failedFilter && failedFilter->match(categoryName); +} + +InFlightRemark RemarkEngine::emitOptimizationRemark(Location loc, + RemarkOpts opts) { + return emitIfEnabled(loc, opts, + &RemarkEngine::isPassedOptRemarkEnabled); +} + +InFlightRemark RemarkEngine::emitOptimizationRemarkMiss(Location loc, + RemarkOpts opts) { + return emitIfEnabled( + loc, opts, &RemarkEngine::isMissedOptRemarkEnabled); +} + +InFlightRemark RemarkEngine::emitOptimizationRemarkFailure(Location loc, + RemarkOpts opts) { + return emitIfEnabled( + loc, opts, &RemarkEngine::isFailedOptRemarkEnabled); +} + +InFlightRemark RemarkEngine::emitOptimizationRemarkAnalysis(Location loc, + RemarkOpts opts) { + return emitIfEnabled( + loc, opts, &RemarkEngine::isAnalysisOptRemarkEnabled); +} + +//===----------------------------------------------------------------------===// +// RemarkEngine +//===----------------------------------------------------------------------===// + +void RemarkEngine::report(const Remark &&remark) { + // Stream the remark + if (remarkStreamer) + remarkStreamer->streamOptimizationRemark(remark); + + // Print using MLIR's diagnostic + if (printAsEmitRemarks) + emitRemark(remark.getLocation(), remark.getMsg()); +} + +RemarkEngine::~RemarkEngine() { + if (remarkStreamer) + remarkStreamer->finalize(); +} + +llvm::LogicalResult +RemarkEngine::initialize(std::unique_ptr streamer, + std::string *errMsg) { + // If you need to validate categories/filters, do so here and set errMsg. + remarkStreamer = std::move(streamer); + return success(); +} + +RemarkEngine::RemarkEngine(bool printAsEmitRemarks, + const RemarkCategories &cats) + : printAsEmitRemarks(printAsEmitRemarks) { + if (cats.passed) + passedFilter = llvm::Regex(cats.passed.value()); + if (cats.missed) + missFilter = llvm::Regex(cats.missed.value()); + if (cats.analysis) + analysisFilter = llvm::Regex(cats.analysis.value()); + if (cats.failed) + failedFilter = llvm::Regex(cats.failed.value()); +} + +llvm::LogicalResult mlir::remark::enableOptimizationRemarks( + MLIRContext &ctx, + std::unique_ptr streamer, + const remark::RemarkCategories &cats, bool printAsEmitRemarks) { + auto engine = + std::make_unique(printAsEmitRemarks, cats); + + std::string errMsg; + if (failed(engine->initialize(std::move(streamer), &errMsg))) { + llvm::report_fatal_error( + llvm::Twine("Failed to initialize remark engine. Error: ") + errMsg); + } + ctx.setRemarkEngine(std::move(engine)); + + return success(); +} diff --git a/mlir/lib/Remark/CMakeLists.txt b/mlir/lib/Remark/CMakeLists.txt new file mode 100644 index 000000000000..920a95db4f07 --- /dev/null +++ b/mlir/lib/Remark/CMakeLists.txt @@ -0,0 +1,14 @@ +add_mlir_library(MLIRRemarkStreamer + RemarkStreamer.cpp + + ADDITIONAL_HEADER_DIRS + ${MLIR_MAIN_INCLUDE_DIR}/mlir/Remark + + LINK_LIBS PUBLIC + MLIRIR + + LINK_COMPONENTS + Remarks + Core + BitstreamReader + ) diff --git a/mlir/lib/Remark/RemarkStreamer.cpp b/mlir/lib/Remark/RemarkStreamer.cpp new file mode 100644 index 000000000000..8e3544ff2c34 --- /dev/null +++ b/mlir/lib/Remark/RemarkStreamer.cpp @@ -0,0 +1,69 @@ +#include "mlir/Remark/RemarkStreamer.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Remarks.h" + +#include "llvm/Remarks/RemarkSerializer.h" +#include "llvm/Remarks/RemarkStreamer.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/ToolOutputFile.h" + +namespace mlir::remark::detail { + +FailureOr> +LLVMRemarkStreamer::createToFile(llvm::StringRef path, + llvm::remarks::Format fmt) { + std::error_code ec; + // Use error_code ctor; YAML is text. (Bitstream also works fine here.) + auto f = + std::make_unique(path, ec, llvm::sys::fs::OF_Text); + if (ec) + return failure(); + + auto serOr = llvm::remarks::createRemarkSerializer( + fmt, llvm::remarks::SerializerMode::Separate, f->os()); + if (!serOr) { + llvm::consumeError(serOr.takeError()); + return failure(); + } + + auto rs = + std::make_unique(std::move(*serOr), path); + + auto impl = std::unique_ptr(new LLVMRemarkStreamer()); + impl->remarkStreamer = std::move(rs); + impl->file = std::move(f); + return std::unique_ptr(std::move(impl)); +} + +void LLVMRemarkStreamer::streamOptimizationRemark(const Remark &remark) { + if (!remarkStreamer->matchesFilter(remark.getCategoryName())) + return; + + // First, convert the diagnostic to a remark. + llvm::remarks::Remark r = remark.generateRemark(); + // Then, emit the remark through the serializer. + remarkStreamer->getSerializer().emit(r); +} + +LLVMRemarkStreamer::~LLVMRemarkStreamer() { + if (file && remarkStreamer) + file->keep(); +} +} // namespace mlir::remark::detail + +namespace mlir::remark { +LogicalResult enableOptimizationRemarksWithLLVMStreamer( + MLIRContext &ctx, StringRef path, llvm::remarks::Format fmt, + const RemarkCategories &cat, bool printAsEmitRemarks) { + + FailureOr> sOr = + detail::LLVMRemarkStreamer::createToFile(path, fmt); + if (failed(sOr)) + return failure(); + + return remark::enableOptimizationRemarks(ctx, std::move(*sOr), cat, + printAsEmitRemarks); +} + +} // namespace mlir::remark diff --git a/mlir/unittests/IR/CMakeLists.txt b/mlir/unittests/IR/CMakeLists.txt index a46e64718dab..75cd2d65ef5a 100644 --- a/mlir/unittests/IR/CMakeLists.txt +++ b/mlir/unittests/IR/CMakeLists.txt @@ -14,6 +14,7 @@ add_mlir_unittest(MLIRIRTests MemrefLayoutTest.cpp OperationSupportTest.cpp PatternMatchTest.cpp + RemarkTest.cpp ShapedTypeTest.cpp SymbolTableTest.cpp TypeTest.cpp @@ -28,3 +29,4 @@ add_mlir_unittest(MLIRIRTests target_include_directories(MLIRIRTests PRIVATE "${MLIR_BINARY_DIR}/test/lib/Dialect/Test") mlir_target_link_libraries(MLIRIRTests PRIVATE MLIRIR) target_link_libraries(MLIRIRTests PRIVATE MLIRTestDialect) +target_link_libraries(MLIRIRTests PRIVATE MLIRRemarkStreamer) diff --git a/mlir/unittests/IR/RemarkTest.cpp b/mlir/unittests/IR/RemarkTest.cpp new file mode 100644 index 000000000000..65e1e08b8383 --- /dev/null +++ b/mlir/unittests/IR/RemarkTest.cpp @@ -0,0 +1,315 @@ +//===- RemarkTest.cpp - Remark unit tests -------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/Remarks.h" +#include "mlir/Remark/RemarkStreamer.h" +#include "mlir/Support/TypeID.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/IR/LLVMRemarkStreamer.h" +#include "llvm/Remarks/RemarkFormat.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/LogicalResult.h" +#include "llvm/Support/YAMLParser.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include + +using namespace llvm; +using namespace mlir; +using namespace testing; +namespace { + +TEST(Remark, TestOutputOptimizationRemark) { + std::string categoryVectorizer("Vectorizer"); + std::string categoryRegister("Register"); + std::string categoryUnroll("Unroll"); + std::string categoryInliner("Inliner"); + std::string categoryReroller("Reroller"); + std::string myPassname1("myPass1"); + SmallString<64> tmpPathStorage; + sys::fs::createUniquePath("remarks-%%%%%%.yaml", tmpPathStorage, + /*MakeAbsolute=*/true); + std::string yamlFile = + std::string(tmpPathStorage.data(), tmpPathStorage.size()); + ASSERT_FALSE(yamlFile.empty()); + + { + MLIRContext context; + Location loc = UnknownLoc::get(&context); + + context.printOpOnDiagnostic(true); + context.printStackTraceOnDiagnostic(true); + + // Setup the remark engine + mlir::remark::RemarkCategories cats{/*passed=*/categoryVectorizer, + /*missed=*/categoryUnroll, + /*analysis=*/categoryRegister, + /*failed=*/categoryInliner}; + + LogicalResult isEnabled = + mlir::remark::enableOptimizationRemarksWithLLVMStreamer( + context, yamlFile, llvm::remarks::Format::YAML, cats); + ASSERT_TRUE(succeeded(isEnabled)) << "Failed to enable remark engine"; + + // PASS: something succeeded + remark::passed(loc, remark::RemarkOpts::name("Pass1") + .category(categoryVectorizer) + .subCategory(myPassname1) + .function("bar")) + << "vectorized loop" << remark::metric("tripCount", 128); + + // ANALYSIS: neutral insight + remark::analysis( + loc, remark::RemarkOpts::name("Analysis1").category(categoryRegister)) + << "Kernel uses 168 registers"; + + // MISSED: explain why + suggest a fix + remark::missed(loc, remark::RemarkOpts::name("Miss1") + .category(categoryUnroll) + .subCategory(myPassname1)) + << remark::reason("not profitable at this size") + << remark::suggest("increase unroll factor to >=4"); + + // FAILURE: action attempted but failed + remark::failed(loc, remark::RemarkOpts::name("Failed1") + .category(categoryInliner) + .subCategory(myPassname1)) + << remark::reason("failed due to unsupported pattern"); + + // FAILURE: Won't show up + remark::failed(loc, remark::RemarkOpts::name("Failed2") + .category(categoryReroller) + .subCategory(myPassname1)) + << remark::reason("failed due to rerolling pattern"); + } + + // Read the file + auto bufferOrErr = MemoryBuffer::getFile(yamlFile); + ASSERT_TRUE(static_cast(bufferOrErr)) << "Failed to open remarks file"; + std::string content = bufferOrErr.get()->getBuffer().str(); + + EXPECT_THAT(content, HasSubstr("--- !Passed")); + EXPECT_THAT(content, HasSubstr("Name: Pass1")); + EXPECT_THAT(content, HasSubstr("Pass: 'Vectorizer:myPass1'")); + EXPECT_THAT(content, HasSubstr("Function: bar")); + EXPECT_THAT(content, HasSubstr("Remark: vectorized loop")); + EXPECT_THAT(content, HasSubstr("tripCount: '128'")); + + EXPECT_THAT(content, HasSubstr("--- !Analysis")); + EXPECT_THAT(content, HasSubstr("Pass: Register")); + EXPECT_THAT(content, HasSubstr("Name: Analysis1")); + EXPECT_THAT(content, HasSubstr("Function: ''")); + EXPECT_THAT(content, HasSubstr("Remark: Kernel uses 168 registers")); + + EXPECT_THAT(content, HasSubstr("--- !Missed")); + EXPECT_THAT(content, HasSubstr("Pass: 'Unroll:myPass1'")); + EXPECT_THAT(content, HasSubstr("Name: Miss1")); + EXPECT_THAT(content, HasSubstr("Function: ''")); + EXPECT_THAT(content, + HasSubstr("Reason: not profitable at this size")); + EXPECT_THAT(content, + HasSubstr("Suggestion: 'increase unroll factor to >=4'")); + + EXPECT_THAT(content, HasSubstr("--- !Failure")); + EXPECT_THAT(content, HasSubstr("Pass: 'Inliner:myPass1'")); + EXPECT_THAT(content, HasSubstr("Name: Failed1")); + EXPECT_THAT(content, HasSubstr("Function: ''")); + EXPECT_THAT(content, + HasSubstr("Reason: failed due to unsupported pattern")); + + EXPECT_THAT(content, Not(HasSubstr("Failed2"))); + EXPECT_THAT(content, Not(HasSubstr("Reroller"))); + + // Also verify document order to avoid false positives. + size_t iPassed = content.find("--- !Passed"); + size_t iAnalysis = content.find("--- !Analysis"); + size_t iMissed = content.find("--- !Missed"); + size_t iFailure = content.find("--- !Failure"); + + ASSERT_NE(iPassed, std::string::npos); + ASSERT_NE(iAnalysis, std::string::npos); + ASSERT_NE(iMissed, std::string::npos); + ASSERT_NE(iFailure, std::string::npos); + + EXPECT_LT(iPassed, iAnalysis); + EXPECT_LT(iAnalysis, iMissed); + EXPECT_LT(iMissed, iFailure); +} + +TEST(Remark, TestNoOutputOptimizationRemark) { + const auto *pass1Msg = "My message"; + + std::string categoryFailName("myImportantCategory"); + std::string myPassname1("myPass1"); + std::string funcName("myFunc"); + SmallString<64> tmpPathStorage; + sys::fs::createUniquePath("remarks-%%%%%%.yaml", tmpPathStorage, + /*MakeAbsolute=*/true); + std::string yamlFile = + std::string(tmpPathStorage.data(), tmpPathStorage.size()); + ASSERT_FALSE(yamlFile.empty()); + std::error_code ec = + llvm::sys::fs::remove(yamlFile, /*IgnoreNonExisting=*/true); + if (ec) { + FAIL() << "Failed to remove file " << yamlFile << ": " << ec.message(); + } + { + MLIRContext context; + Location loc = UnknownLoc::get(&context); + remark::failed(loc, remark::RemarkOpts::name("myfail") + .category(categoryFailName) + .subCategory(myPassname1)) + << remark::reason(pass1Msg); + } + // No setup, so no output file should be created + // check! + bool fileExists = llvm::sys::fs::exists(yamlFile); + EXPECT_FALSE(fileExists) + << "Expected no YAML file to be created without setupOptimizationRemarks"; +} + +TEST(Remark, TestOutputOptimizationRemarkDiagnostic) { + std::string categoryVectorizer("Vectorizer"); + std::string categoryRegister("Register"); + std::string categoryUnroll("Unroll"); + std::string myPassname1("myPass1"); + std::string fName("foo"); + + llvm::SmallVector seenMsg; + { + MLIRContext context; + Location loc = UnknownLoc::get(&context); + + context.printOpOnDiagnostic(true); + context.printStackTraceOnDiagnostic(true); + + // Register a handler that captures the diagnostic. + ScopedDiagnosticHandler handler(&context, [&](Diagnostic &diag) { + seenMsg.push_back(diag.str()); + return success(); + }); + + // Setup the remark engine + mlir::remark::RemarkCategories cats{/*passed=*/categoryVectorizer, + /*missed=*/categoryUnroll, + /*analysis=*/categoryRegister, + /*failed=*/categoryUnroll}; + + LogicalResult isEnabled = + remark::enableOptimizationRemarks(context, nullptr, cats, true); + + ASSERT_TRUE(succeeded(isEnabled)) << "Failed to enable remark engine"; + + // PASS: something succeeded + remark::passed(loc, remark::RemarkOpts::name("pass1") + .category(categoryVectorizer) + .function(fName) + .subCategory(myPassname1)) + << "vectorized loop" << remark::metric("tripCount", 128); + + // ANALYSIS: neutral insight + remark::analysis(loc, remark::RemarkOpts::name("Analysis1") + .category(categoryRegister) + .function(fName)) + << "Kernel uses 168 registers"; + + // MISSED: explain why + suggest a fix + int target = 128; + int tripBad = 4; + int threshold = 256; + + remark::missed(loc, {"", categoryUnroll, "unroller2", ""}) + << remark::reason("tripCount={0} < threshold={1}", tripBad, threshold); + + remark::missed(loc, {"", categoryUnroll, "", ""}) + << remark::reason("tripCount={0} < threshold={1}", tripBad, threshold) + << remark::suggest("increase unroll to {0}", target); + + // FAILURE: action attempted but failed + remark::failed(loc, {"", categoryUnroll, "", ""}) + << remark::reason("failed due to unsupported pattern"); + } + // clang-format off + unsigned long expectedSize = 5; + ASSERT_EQ(seenMsg.size(), expectedSize); + EXPECT_EQ(seenMsg[0], "[Passed] pass1 | Category:Vectorizer:myPass1 | Function=foo | Remark=\"vectorized loop\", tripCount=128"); + EXPECT_EQ(seenMsg[1], "[Analysis] Analysis1 | Category:Register | Function=foo | Remark=\"Kernel uses 168 registers\""); + EXPECT_EQ(seenMsg[2], "[Missed] | Category:Unroll:unroller2 | Reason=\"tripCount=4 < threshold=256\""); + EXPECT_EQ(seenMsg[3], "[Missed] | Category:Unroll | Reason=\"tripCount=4 < threshold=256\", Suggestion=\"increase unroll to 128\""); + EXPECT_EQ(seenMsg[4], "[Failure] | Category:Unroll | Reason=\"failed due to unsupported pattern\""); + // clang-format on +} + +/// Custom remark streamer that prints remarks to stderr. +class MyCustomStreamer : public remark::detail::MLIRRemarkStreamerBase { +public: + MyCustomStreamer() = default; + + void streamOptimizationRemark(const remark::detail::Remark &remark) override { + llvm::errs() << "Custom remark: "; + remark.print(llvm::errs(), true); + llvm::errs() << "\n"; + } +}; + +TEST(Remark, TestCustomOptimizationRemarkDiagnostic) { + testing::internal::CaptureStderr(); + const auto *pass1Msg = "My message"; + const auto *pass2Msg = "My another message"; + const auto *pass3Msg = "Do not show this message"; + + std::string categoryLoopunroll("LoopUnroll"); + std::string categoryInline("Inliner"); + std::string myPassname1("myPass1"); + std::string myPassname2("myPass2"); + std::string funcName("myFunc"); + + std::string seenMsg = ""; + + { + MLIRContext context; + Location loc = UnknownLoc::get(&context); + + // Setup the remark engine + mlir::remark::RemarkCategories cats{/*passed=*/categoryLoopunroll, + /*missed=*/std::nullopt, + /*analysis=*/std::nullopt, + /*failed=*/categoryLoopunroll}; + + LogicalResult isEnabled = remark::enableOptimizationRemarks( + context, std::make_unique(), cats, true); + ASSERT_TRUE(succeeded(isEnabled)) << "Failed to enable remark engine"; + + // Remark 1: pass, category LoopUnroll + remark::passed(loc, {"", categoryLoopunroll, myPassname1, ""}) << pass1Msg; + // Remark 2: failure, category LoopUnroll + remark::failed(loc, {"", categoryLoopunroll, myPassname2, ""}) + << remark::reason(pass2Msg); + // Remark 3: pass, category Inline (should not be printed) + remark::passed(loc, {"", categoryInline, myPassname1, ""}) << pass3Msg; + } + + llvm::errs().flush(); + std::string errOut = ::testing::internal::GetCapturedStderr(); + + // Expect exactly two "Custom remark:" lines. + auto first = errOut.find("Custom remark:"); + EXPECT_NE(first, std::string::npos); + auto second = errOut.find("Custom remark:", first + 1); + EXPECT_NE(second, std::string::npos); + auto third = errOut.find("Custom remark:", second + 1); + EXPECT_EQ(third, std::string::npos); + + // Containment checks for messages. + EXPECT_NE(errOut.find(pass1Msg), std::string::npos); // printed + EXPECT_NE(errOut.find(pass2Msg), std::string::npos); // printed + EXPECT_EQ(errOut.find(pass3Msg), std::string::npos); // filtered out +} +} // namespace