[lld][COFF] Add optimization remarks options to lld-link (#205390)

Add support for emitting optimization remarks during LTO in the COFF
 linker, matching the existing ELF linker functionality. The following
 options are added:

   -opt-remarks-filename
   -opt-remarks-passes
   -opt-remarks-format
   -opt-remarks-with-hotness
   -opt-remarks-hotness-threshold

 These options are forwarded to the LTO backend via lto::Config and
 allow users to capture optimization decisions (e.g., inlining) as
 structured YAML output, optionally filtered by pass name or profile
 hotness threshold.

Assisted by Claude.

GitOrigin-RevId: a037d2ad7312adc58664358893c20927efae22cf
diff --git a/COFF/Config.h b/COFF/Config.h
index f22d580..7546da2 100644
--- a/COFF/Config.h
+++ b/COFF/Config.h
@@ -21,6 +21,7 @@
 #include "llvm/Support/VirtualFileSystem.h"
 #include <cstdint>
 #include <map>
+#include <optional>
 #include <string>
 
 namespace lld::coff {
@@ -357,6 +358,11 @@
   EmitKind emit = EmitKind::Obj;
   bool allowDuplicateWeak = false;
   BuildIDHash buildIDHash = BuildIDHash::None;
+  llvm::StringRef optRemarksFilename;
+  llvm::StringRef optRemarksPasses;
+  llvm::StringRef optRemarksFormat;
+  bool optRemarksWithHotness = false;
+  std::optional<uint64_t> optRemarksHotnessThreshold = 0;
 };
 
 struct COFFSyncStream : SyncStream {
diff --git a/COFF/Driver.cpp b/COFF/Driver.cpp
index 6583e43..ecbb5e9 100644
--- a/COFF/Driver.cpp
+++ b/COFF/Driver.cpp
@@ -33,6 +33,7 @@
 #include "llvm/Option/Arg.h"
 #include "llvm/Option/ArgList.h"
 #include "llvm/Option/Option.h"
+#include "llvm/Remarks/HotnessThresholdParser.h"
 #include "llvm/Support/BinaryStreamReader.h"
 #include "llvm/Support/CommandLine.h"
 #include "llvm/Support/Debug.h"
@@ -2327,6 +2328,19 @@
   if (args.hasFlag(OPT_prefetch_inputs, OPT_prefetch_inputs_no, false))
     config->prefetchInputs = true;
 
+  config->optRemarksFilename = args.getLastArgValue(OPT_opt_remarks_filename);
+  config->optRemarksPasses = args.getLastArgValue(OPT_opt_remarks_passes);
+  config->optRemarksFormat = args.getLastArgValue(OPT_opt_remarks_format);
+  config->optRemarksWithHotness = args.hasArg(OPT_opt_remarks_with_hotness);
+  if (auto *arg = args.getLastArg(OPT_opt_remarks_hotness_threshold)) {
+    auto resultOrErr = remarks::parseHotnessThresholdOption(arg->getValue());
+    if (!resultOrErr)
+      Err(ctx) << arg->getSpelling() << ": invalid argument '"
+               << arg->getValue() << "', only integer or 'auto' is supported";
+    else
+      config->optRemarksHotnessThreshold = *resultOrErr;
+  }
+
   if (errCount(ctx))
     return;
 
diff --git a/COFF/LTO.cpp b/COFF/LTO.cpp
index 71413b0..f85a260 100644
--- a/COFF/LTO.cpp
+++ b/COFF/LTO.cpp
@@ -95,6 +95,11 @@
   c.SampleProfile = ctx.config.ltoSampleProfileName;
   c.TimeTraceEnabled = ctx.config.timeTraceEnabled;
   c.TimeTraceGranularity = ctx.config.timeTraceGranularity;
+  c.RemarksFilename = ctx.config.optRemarksFilename;
+  c.RemarksPasses = ctx.config.optRemarksPasses;
+  c.RemarksWithHotness = ctx.config.optRemarksWithHotness;
+  c.RemarksHotnessThreshold = ctx.config.optRemarksHotnessThreshold;
+  c.RemarksFormat = ctx.config.optRemarksFormat;
 
   if (ctx.config.emit == EmitKind::LLVM) {
     c.PreCodeGenModuleHook = [this](size_t task, const Module &m) {
diff --git a/COFF/Options.td b/COFF/Options.td
index bc1902d..eb05b2f 100644
--- a/COFF/Options.td
+++ b/COFF/Options.td
@@ -360,6 +360,18 @@
                          "possible, to improve link times",
                          "Do not prefetch input files (default)">;
 
+def opt_remarks_filename: P<"opt-remarks-filename",
+  "YAML output file for optimization remarks">;
+def opt_remarks_passes: P<"opt-remarks-passes",
+  "Regex for the passes that need to be serialized to the output file">;
+def opt_remarks_format: P<"opt-remarks-format",
+  "The format used for serializing remarks (default: YAML)">;
+def opt_remarks_with_hotness: F<"opt-remarks-with-hotness">,
+  HelpText<"Include hotness information in the optimization remarks file">;
+def opt_remarks_hotness_threshold: P<"opt-remarks-hotness-threshold",
+  "Minimum profile count required for an optimization remark to be output. "
+  "Use 'auto' to apply the threshold from profile summary.">;
+
 // Flags for debugging
 def lldmap : F<"lldmap">;
 def lldmap_file : P_priv<"lldmap">;
diff --git a/test/COFF/lto-opt-remarks.ll b/test/COFF/lto-opt-remarks.ll
new file mode 100644
index 0000000..c70e180
--- /dev/null
+++ b/test/COFF/lto-opt-remarks.ll
@@ -0,0 +1,87 @@
+; REQUIRES: x86
+; RUN: llvm-as %s -o %t.obj
+
+; RUN: rm -f %t.yaml %t.pass.yaml %t.hot.yaml %t.t300.yaml %t.t301.yaml
+; RUN: lld-link -opt-remarks-filename:%t.yaml %t.obj -entry:main -nodefaultlib \
+; RUN:   -out:%t.exe -force:unresolved
+; RUN: cat %t.yaml | FileCheck %s -check-prefix=YAML
+
+; RUN: lld-link -opt-remarks-filename:%t.pass.yaml -opt-remarks-passes:inline \
+; RUN:   %t.obj -entry:main -nodefaultlib -out:%t.exe -force:unresolved
+; RUN: cat %t.pass.yaml | FileCheck %s -check-prefix=YAML-PASSES
+
+; RUN: lld-link -opt-remarks-with-hotness -opt-remarks-filename:%t.hot.yaml \
+; RUN:   %t.obj -entry:main -nodefaultlib -out:%t.exe -force:unresolved
+; RUN: cat %t.hot.yaml | FileCheck %s -check-prefix=YAML-HOT
+
+; RUN: lld-link -opt-remarks-with-hotness \
+; RUN:   -opt-remarks-hotness-threshold:300 \
+; RUN:   -opt-remarks-filename:%t.t300.yaml %t.obj -entry:main -nodefaultlib \
+; RUN:   -out:%t.exe -force:unresolved
+; RUN: FileCheck %s -check-prefix=YAML-HOT < %t.t300.yaml
+
+; RUN: lld-link -opt-remarks-with-hotness \
+; RUN:   -opt-remarks-hotness-threshold:301 \
+; RUN:   -opt-remarks-filename:%t.t301.yaml %t.obj -entry:main -nodefaultlib \
+; RUN:   -out:%t.exe -force:unresolved
+; RUN: count 0 < %t.t301.yaml
+
+; RUN: lld-link -opt-remarks-filename:%t.yaml -opt-remarks-format:yaml \
+; RUN:   %t.obj -entry:main -nodefaultlib -out:%t.exe -force:unresolved
+; RUN: FileCheck %s -check-prefix=YAML < %t.yaml
+
+; YAML:      --- !Passed
+; YAML-NEXT: Pass:            inline
+; YAML-NEXT: Name:            Inlined
+; YAML-NEXT: Function:        main
+; YAML-NEXT: Args:
+; YAML-NEXT:   - String:          ''''
+; YAML-NEXT:   - Callee:          tinkywinky
+; YAML-NEXT:   - String:          ''' inlined into '''
+; YAML-NEXT:   - Caller:          main
+; YAML-NEXT:   - String:          ''''
+; YAML-NEXT:   - String:          ' with '
+; YAML-NEXT:   - String:          '(cost='
+; YAML-NEXT:   - Cost:
+; YAML-NEXT:   - String:          ', threshold='
+; YAML-NEXT:   - Threshold:
+; YAML-NEXT:   - String:          ')'
+; YAML-NEXT: ...
+
+; YAML-HOT:      --- !Passed
+; YAML-HOT-NEXT: Pass:            inline
+; YAML-HOT-NEXT: Name:            Inlined
+; YAML-HOT-NEXT: Function:        main
+; YAML-HOT-NEXT: Hotness:         300
+; YAML-HOT-NEXT: Args:
+; YAML-HOT-NEXT:   - String:          ''''
+; YAML-HOT-NEXT:   - Callee:          tinkywinky
+; YAML-HOT-NEXT:   - String:          ''' inlined into '''
+; YAML-HOT-NEXT:   - Caller:          main
+; YAML-HOT-NEXT:   - String:          ''''
+; YAML-HOT-NEXT:   - String:          ' with '
+; YAML-HOT-NEXT:   - String:          '(cost='
+; YAML-HOT-NEXT:   - Cost:
+; YAML-HOT-NEXT:   - String:          ', threshold='
+; YAML-HOT-NEXT:   - Threshold:
+; YAML-HOT-NEXT:   - String:          ')'
+; YAML-HOT-NEXT: ...
+
+; YAML-PASSES: Pass:            inline
+
+target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-pc-windows-msvc19.14.26433"
+
+declare i32 @patatino()
+
+define i32 @tinkywinky() {
+  %a = call i32 @patatino()
+  ret i32 %a
+}
+
+define i32 @main() !prof !0 {
+  %i = call i32 @tinkywinky()
+  ret i32 %i
+}
+
+!0 = !{!"function_entry_count", i64 300}