Home | History | Annotate | Line # | Download | only in lib
      1 //===-- Analysis.cpp --------------------------------------------*- C++ -*-===//
      2 //
      3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
      4 // See https://llvm.org/LICENSE.txt for license information.
      5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
      6 //
      7 //===----------------------------------------------------------------------===//
      8 
      9 #include "Analysis.h"
     10 #include "BenchmarkResult.h"
     11 #include "llvm/ADT/STLExtras.h"
     12 #include "llvm/MC/MCAsmInfo.h"
     13 #include "llvm/MC/MCTargetOptions.h"
     14 #include "llvm/Support/FormatVariadic.h"
     15 #include <limits>
     16 #include <unordered_set>
     17 #include <vector>
     18 
     19 namespace llvm {
     20 namespace exegesis {
     21 
     22 static const char kCsvSep = ',';
     23 
     24 namespace {
     25 
     26 enum EscapeTag { kEscapeCsv, kEscapeHtml, kEscapeHtmlString };
     27 
     28 template <EscapeTag Tag> void writeEscaped(raw_ostream &OS, const StringRef S);
     29 
     30 template <> void writeEscaped<kEscapeCsv>(raw_ostream &OS, const StringRef S) {
     31   if (!llvm::is_contained(S, kCsvSep)) {
     32     OS << S;
     33   } else {
     34     // Needs escaping.
     35     OS << '"';
     36     for (const char C : S) {
     37       if (C == '"')
     38         OS << "\"\"";
     39       else
     40         OS << C;
     41     }
     42     OS << '"';
     43   }
     44 }
     45 
     46 template <> void writeEscaped<kEscapeHtml>(raw_ostream &OS, const StringRef S) {
     47   for (const char C : S) {
     48     if (C == '<')
     49       OS << "&lt;";
     50     else if (C == '>')
     51       OS << "&gt;";
     52     else if (C == '&')
     53       OS << "&amp;";
     54     else
     55       OS << C;
     56   }
     57 }
     58 
     59 template <>
     60 void writeEscaped<kEscapeHtmlString>(raw_ostream &OS, const StringRef S) {
     61   for (const char C : S) {
     62     if (C == '"')
     63       OS << "\\\"";
     64     else
     65       OS << C;
     66   }
     67 }
     68 
     69 } // namespace
     70 
     71 template <EscapeTag Tag>
     72 static void
     73 writeClusterId(raw_ostream &OS,
     74                const InstructionBenchmarkClustering::ClusterId &CID) {
     75   if (CID.isNoise())
     76     writeEscaped<Tag>(OS, "[noise]");
     77   else if (CID.isError())
     78     writeEscaped<Tag>(OS, "[error]");
     79   else
     80     OS << CID.getId();
     81 }
     82 
     83 template <EscapeTag Tag>
     84 static void writeMeasurementValue(raw_ostream &OS, const double Value) {
     85   // Given Value, if we wanted to serialize it to a string,
     86   // how many base-10 digits will we need to store, max?
     87   static constexpr auto MaxDigitCount =
     88       std::numeric_limits<decltype(Value)>::max_digits10;
     89   // Also, we will need a decimal separator.
     90   static constexpr auto DecimalSeparatorLen = 1; // '.' e.g.
     91   // So how long of a string will the serialization produce, max?
     92   static constexpr auto SerializationLen = MaxDigitCount + DecimalSeparatorLen;
     93 
     94   // WARNING: when changing the format, also adjust the small-size estimate ^.
     95   static constexpr StringLiteral SimpleFloatFormat = StringLiteral("{0:F}");
     96 
     97   writeEscaped<Tag>(
     98       OS, formatv(SimpleFloatFormat.data(), Value).sstr<SerializationLen>());
     99 }
    100 
    101 template <typename EscapeTag, EscapeTag Tag>
    102 void Analysis::writeSnippet(raw_ostream &OS, ArrayRef<uint8_t> Bytes,
    103                             const char *Separator) const {
    104   SmallVector<std::string, 3> Lines;
    105   // Parse the asm snippet and print it.
    106   while (!Bytes.empty()) {
    107     MCInst MI;
    108     uint64_t MISize = 0;
    109     if (!Disasm_->getInstruction(MI, MISize, Bytes, 0, nulls())) {
    110       writeEscaped<Tag>(OS, join(Lines, Separator));
    111       writeEscaped<Tag>(OS, Separator);
    112       writeEscaped<Tag>(OS, "[error decoding asm snippet]");
    113       return;
    114     }
    115     SmallString<128> InstPrinterStr; // FIXME: magic number.
    116     raw_svector_ostream OSS(InstPrinterStr);
    117     InstPrinter_->printInst(&MI, 0, "", *SubtargetInfo_, OSS);
    118     Bytes = Bytes.drop_front(MISize);
    119     Lines.emplace_back(StringRef(InstPrinterStr).trim());
    120   }
    121   writeEscaped<Tag>(OS, join(Lines, Separator));
    122 }
    123 
    124 // Prints a row representing an instruction, along with scheduling info and
    125 // point coordinates (measurements).
    126 void Analysis::printInstructionRowCsv(const size_t PointId,
    127                                       raw_ostream &OS) const {
    128   const InstructionBenchmark &Point = Clustering_.getPoints()[PointId];
    129   writeClusterId<kEscapeCsv>(OS, Clustering_.getClusterIdForPoint(PointId));
    130   OS << kCsvSep;
    131   writeSnippet<EscapeTag, kEscapeCsv>(OS, Point.AssembledSnippet, "; ");
    132   OS << kCsvSep;
    133   writeEscaped<kEscapeCsv>(OS, Point.Key.Config);
    134   OS << kCsvSep;
    135   assert(!Point.Key.Instructions.empty());
    136   const MCInst &MCI = Point.keyInstruction();
    137   unsigned SchedClassId;
    138   std::tie(SchedClassId, std::ignore) = ResolvedSchedClass::resolveSchedClassId(
    139       *SubtargetInfo_, *InstrInfo_, MCI);
    140 #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
    141   const MCSchedClassDesc *const SCDesc =
    142       SubtargetInfo_->getSchedModel().getSchedClassDesc(SchedClassId);
    143   writeEscaped<kEscapeCsv>(OS, SCDesc->Name);
    144 #else
    145   OS << SchedClassId;
    146 #endif
    147   for (const auto &Measurement : Point.Measurements) {
    148     OS << kCsvSep;
    149     writeMeasurementValue<kEscapeCsv>(OS, Measurement.PerInstructionValue);
    150   }
    151   OS << "\n";
    152 }
    153 
    154 Analysis::Analysis(const Target &Target, std::unique_ptr<MCInstrInfo> InstrInfo,
    155                    const InstructionBenchmarkClustering &Clustering,
    156                    double AnalysisInconsistencyEpsilon,
    157                    bool AnalysisDisplayUnstableOpcodes,
    158                    const std::string &ForceCpuName)
    159     : Clustering_(Clustering), InstrInfo_(std::move(InstrInfo)),
    160       AnalysisInconsistencyEpsilonSquared_(AnalysisInconsistencyEpsilon *
    161                                            AnalysisInconsistencyEpsilon),
    162       AnalysisDisplayUnstableOpcodes_(AnalysisDisplayUnstableOpcodes) {
    163   if (Clustering.getPoints().empty())
    164     return;
    165 
    166   const InstructionBenchmark &FirstPoint = Clustering.getPoints().front();
    167   const std::string CpuName =
    168       ForceCpuName.empty() ? FirstPoint.CpuName : ForceCpuName;
    169   RegInfo_.reset(Target.createMCRegInfo(FirstPoint.LLVMTriple));
    170   MCTargetOptions MCOptions;
    171   AsmInfo_.reset(
    172       Target.createMCAsmInfo(*RegInfo_, FirstPoint.LLVMTriple, MCOptions));
    173   SubtargetInfo_.reset(
    174       Target.createMCSubtargetInfo(FirstPoint.LLVMTriple, CpuName, ""));
    175   InstPrinter_.reset(Target.createMCInstPrinter(
    176       Triple(FirstPoint.LLVMTriple), 0 /*default variant*/, *AsmInfo_,
    177       *InstrInfo_, *RegInfo_));
    178 
    179   Context_ =
    180       std::make_unique<MCContext>(Triple(FirstPoint.LLVMTriple), AsmInfo_.get(),
    181                                   RegInfo_.get(), SubtargetInfo_.get());
    182   Disasm_.reset(Target.createMCDisassembler(*SubtargetInfo_, *Context_));
    183   assert(Disasm_ && "cannot create MCDisassembler. missing call to "
    184                     "InitializeXXXTargetDisassembler ?");
    185 }
    186 
    187 template <>
    188 Error Analysis::run<Analysis::PrintClusters>(raw_ostream &OS) const {
    189   if (Clustering_.getPoints().empty())
    190     return Error::success();
    191 
    192   // Write the header.
    193   OS << "cluster_id" << kCsvSep << "opcode_name" << kCsvSep << "config"
    194      << kCsvSep << "sched_class";
    195   for (const auto &Measurement : Clustering_.getPoints().front().Measurements) {
    196     OS << kCsvSep;
    197     writeEscaped<kEscapeCsv>(OS, Measurement.Key);
    198   }
    199   OS << "\n";
    200 
    201   // Write the points.
    202   for (const auto &ClusterIt : Clustering_.getValidClusters()) {
    203     for (const size_t PointId : ClusterIt.PointIndices) {
    204       printInstructionRowCsv(PointId, OS);
    205     }
    206     OS << "\n\n";
    207   }
    208   return Error::success();
    209 }
    210 
    211 Analysis::ResolvedSchedClassAndPoints::ResolvedSchedClassAndPoints(
    212     ResolvedSchedClass &&RSC)
    213     : RSC(std::move(RSC)) {}
    214 
    215 std::vector<Analysis::ResolvedSchedClassAndPoints>
    216 Analysis::makePointsPerSchedClass() const {
    217   std::vector<ResolvedSchedClassAndPoints> Entries;
    218   // Maps SchedClassIds to index in result.
    219   std::unordered_map<unsigned, size_t> SchedClassIdToIndex;
    220   const auto &Points = Clustering_.getPoints();
    221   for (size_t PointId = 0, E = Points.size(); PointId < E; ++PointId) {
    222     const InstructionBenchmark &Point = Points[PointId];
    223     if (!Point.Error.empty())
    224       continue;
    225     assert(!Point.Key.Instructions.empty());
    226     // FIXME: we should be using the tuple of classes for instructions in the
    227     // snippet as key.
    228     const MCInst &MCI = Point.keyInstruction();
    229     unsigned SchedClassId;
    230     bool WasVariant;
    231     std::tie(SchedClassId, WasVariant) =
    232         ResolvedSchedClass::resolveSchedClassId(*SubtargetInfo_, *InstrInfo_,
    233                                                 MCI);
    234     const auto IndexIt = SchedClassIdToIndex.find(SchedClassId);
    235     if (IndexIt == SchedClassIdToIndex.end()) {
    236       // Create a new entry.
    237       SchedClassIdToIndex.emplace(SchedClassId, Entries.size());
    238       ResolvedSchedClassAndPoints Entry(
    239           ResolvedSchedClass(*SubtargetInfo_, SchedClassId, WasVariant));
    240       Entry.PointIds.push_back(PointId);
    241       Entries.push_back(std::move(Entry));
    242     } else {
    243       // Append to the existing entry.
    244       Entries[IndexIt->second].PointIds.push_back(PointId);
    245     }
    246   }
    247   return Entries;
    248 }
    249 
    250 // Parallel benchmarks repeat the same opcode multiple times. Just show this
    251 // opcode and show the whole snippet only on hover.
    252 static void writeParallelSnippetHtml(raw_ostream &OS,
    253                                  const std::vector<MCInst> &Instructions,
    254                                  const MCInstrInfo &InstrInfo) {
    255   if (Instructions.empty())
    256     return;
    257   writeEscaped<kEscapeHtml>(OS, InstrInfo.getName(Instructions[0].getOpcode()));
    258   if (Instructions.size() > 1)
    259     OS << " (x" << Instructions.size() << ")";
    260 }
    261 
    262 // Latency tries to find a serial path. Just show the opcode path and show the
    263 // whole snippet only on hover.
    264 static void writeLatencySnippetHtml(raw_ostream &OS,
    265                                     const std::vector<MCInst> &Instructions,
    266                                     const MCInstrInfo &InstrInfo) {
    267   bool First = true;
    268   for (const MCInst &Instr : Instructions) {
    269     if (First)
    270       First = false;
    271     else
    272       OS << " &rarr; ";
    273     writeEscaped<kEscapeHtml>(OS, InstrInfo.getName(Instr.getOpcode()));
    274   }
    275 }
    276 
    277 void Analysis::printPointHtml(const InstructionBenchmark &Point,
    278                               llvm::raw_ostream &OS) const {
    279   OS << "<li><span class=\"mono\" title=\"";
    280   writeSnippet<EscapeTag, kEscapeHtmlString>(OS, Point.AssembledSnippet, "\n");
    281   OS << "\">";
    282   switch (Point.Mode) {
    283   case InstructionBenchmark::Latency:
    284     writeLatencySnippetHtml(OS, Point.Key.Instructions, *InstrInfo_);
    285     break;
    286   case InstructionBenchmark::Uops:
    287   case InstructionBenchmark::InverseThroughput:
    288     writeParallelSnippetHtml(OS, Point.Key.Instructions, *InstrInfo_);
    289     break;
    290   default:
    291     llvm_unreachable("invalid mode");
    292   }
    293   OS << "</span> <span class=\"mono\">";
    294   writeEscaped<kEscapeHtml>(OS, Point.Key.Config);
    295   OS << "</span></li>";
    296 }
    297 
    298 void Analysis::printSchedClassClustersHtml(
    299     const std::vector<SchedClassCluster> &Clusters,
    300     const ResolvedSchedClass &RSC, raw_ostream &OS) const {
    301   const auto &Points = Clustering_.getPoints();
    302   OS << "<table class=\"sched-class-clusters\">";
    303   OS << "<tr><th>ClusterId</th><th>Opcode/Config</th>";
    304   assert(!Clusters.empty());
    305   for (const auto &Measurement :
    306        Points[Clusters[0].getPointIds()[0]].Measurements) {
    307     OS << "<th>";
    308     writeEscaped<kEscapeHtml>(OS, Measurement.Key);
    309     OS << "</th>";
    310   }
    311   OS << "</tr>";
    312   for (const SchedClassCluster &Cluster : Clusters) {
    313     OS << "<tr class=\""
    314        << (Cluster.measurementsMatch(*SubtargetInfo_, RSC, Clustering_,
    315                                      AnalysisInconsistencyEpsilonSquared_)
    316                ? "good-cluster"
    317                : "bad-cluster")
    318        << "\"><td>";
    319     writeClusterId<kEscapeHtml>(OS, Cluster.id());
    320     OS << "</td><td><ul>";
    321     for (const size_t PointId : Cluster.getPointIds()) {
    322       printPointHtml(Points[PointId], OS);
    323     }
    324     OS << "</ul></td>";
    325     for (const auto &Stats : Cluster.getCentroid().getStats()) {
    326       OS << "<td class=\"measurement\">";
    327       writeMeasurementValue<kEscapeHtml>(OS, Stats.avg());
    328       OS << "<br><span class=\"minmax\">[";
    329       writeMeasurementValue<kEscapeHtml>(OS, Stats.min());
    330       OS << ";";
    331       writeMeasurementValue<kEscapeHtml>(OS, Stats.max());
    332       OS << "]</span></td>";
    333     }
    334     OS << "</tr>";
    335   }
    336   OS << "</table>";
    337 }
    338 
    339 void Analysis::SchedClassCluster::addPoint(
    340     size_t PointId, const InstructionBenchmarkClustering &Clustering) {
    341   PointIds.push_back(PointId);
    342   const auto &Point = Clustering.getPoints()[PointId];
    343   if (ClusterId.isUndef())
    344     ClusterId = Clustering.getClusterIdForPoint(PointId);
    345   assert(ClusterId == Clustering.getClusterIdForPoint(PointId));
    346 
    347   Centroid.addPoint(Point.Measurements);
    348 }
    349 
    350 bool Analysis::SchedClassCluster::measurementsMatch(
    351     const MCSubtargetInfo &STI, const ResolvedSchedClass &RSC,
    352     const InstructionBenchmarkClustering &Clustering,
    353     const double AnalysisInconsistencyEpsilonSquared_) const {
    354   assert(!Clustering.getPoints().empty());
    355   const InstructionBenchmark::ModeE Mode = Clustering.getPoints()[0].Mode;
    356 
    357   if (!Centroid.validate(Mode))
    358     return false;
    359 
    360   const std::vector<BenchmarkMeasure> ClusterCenterPoint =
    361       Centroid.getAsPoint();
    362 
    363   const std::vector<BenchmarkMeasure> SchedClassPoint =
    364       RSC.getAsPoint(Mode, STI, Centroid.getStats());
    365   if (SchedClassPoint.empty())
    366     return false; // In Uops mode validate() may not be enough.
    367 
    368   assert(ClusterCenterPoint.size() == SchedClassPoint.size() &&
    369          "Expected measured/sched data dimensions to match.");
    370 
    371   return Clustering.isNeighbour(ClusterCenterPoint, SchedClassPoint,
    372                                 AnalysisInconsistencyEpsilonSquared_);
    373 }
    374 
    375 void Analysis::printSchedClassDescHtml(const ResolvedSchedClass &RSC,
    376                                        raw_ostream &OS) const {
    377   OS << "<table class=\"sched-class-desc\">";
    378   OS << "<tr><th>Valid</th><th>Variant</th><th>NumMicroOps</th><th>Latency</"
    379         "th><th>RThroughput</th><th>WriteProcRes</th><th title=\"This is the "
    380         "idealized unit resource (port) pressure assuming ideal "
    381         "distribution\">Idealized Resource Pressure</th></tr>";
    382   if (RSC.SCDesc->isValid()) {
    383     const auto &SM = SubtargetInfo_->getSchedModel();
    384     OS << "<tr><td>&#10004;</td>";
    385     OS << "<td>" << (RSC.WasVariant ? "&#10004;" : "&#10005;") << "</td>";
    386     OS << "<td>" << RSC.SCDesc->NumMicroOps << "</td>";
    387     // Latencies.
    388     OS << "<td><ul>";
    389     for (int I = 0, E = RSC.SCDesc->NumWriteLatencyEntries; I < E; ++I) {
    390       const auto *const Entry =
    391           SubtargetInfo_->getWriteLatencyEntry(RSC.SCDesc, I);
    392       OS << "<li>" << Entry->Cycles;
    393       if (RSC.SCDesc->NumWriteLatencyEntries > 1) {
    394         // Dismabiguate if more than 1 latency.
    395         OS << " (WriteResourceID " << Entry->WriteResourceID << ")";
    396       }
    397       OS << "</li>";
    398     }
    399     OS << "</ul></td>";
    400     // inverse throughput.
    401     OS << "<td>";
    402     writeMeasurementValue<kEscapeHtml>(
    403         OS,
    404         MCSchedModel::getReciprocalThroughput(*SubtargetInfo_, *RSC.SCDesc));
    405     OS << "</td>";
    406     // WriteProcRes.
    407     OS << "<td><ul>";
    408     for (const auto &WPR : RSC.NonRedundantWriteProcRes) {
    409       OS << "<li><span class=\"mono\">";
    410       writeEscaped<kEscapeHtml>(OS,
    411                                 SM.getProcResource(WPR.ProcResourceIdx)->Name);
    412       OS << "</span>: " << WPR.Cycles << "</li>";
    413     }
    414     OS << "</ul></td>";
    415     // Idealized port pressure.
    416     OS << "<td><ul>";
    417     for (const auto &Pressure : RSC.IdealizedProcResPressure) {
    418       OS << "<li><span class=\"mono\">";
    419       writeEscaped<kEscapeHtml>(OS, SubtargetInfo_->getSchedModel()
    420                                         .getProcResource(Pressure.first)
    421                                         ->Name);
    422       OS << "</span>: ";
    423       writeMeasurementValue<kEscapeHtml>(OS, Pressure.second);
    424       OS << "</li>";
    425     }
    426     OS << "</ul></td>";
    427     OS << "</tr>";
    428   } else {
    429     OS << "<tr><td>&#10005;</td><td></td><td></td></tr>";
    430   }
    431   OS << "</table>";
    432 }
    433 
    434 void Analysis::printClusterRawHtml(
    435     const InstructionBenchmarkClustering::ClusterId &Id, StringRef display_name,
    436     llvm::raw_ostream &OS) const {
    437   const auto &Points = Clustering_.getPoints();
    438   const auto &Cluster = Clustering_.getCluster(Id);
    439   if (Cluster.PointIndices.empty())
    440     return;
    441 
    442   OS << "<div class=\"inconsistency\"><p>" << display_name << " Cluster ("
    443      << Cluster.PointIndices.size() << " points)</p>";
    444   OS << "<table class=\"sched-class-clusters\">";
    445   // Table Header.
    446   OS << "<tr><th>ClusterId</th><th>Opcode/Config</th>";
    447   for (const auto &Measurement : Points[Cluster.PointIndices[0]].Measurements) {
    448     OS << "<th>";
    449     writeEscaped<kEscapeHtml>(OS, Measurement.Key);
    450     OS << "</th>";
    451   }
    452   OS << "</tr>";
    453 
    454   // Point data.
    455   for (const auto &PointId : Cluster.PointIndices) {
    456     OS << "<tr class=\"bad-cluster\"><td>" << display_name << "</td><td><ul>";
    457     printPointHtml(Points[PointId], OS);
    458     OS << "</ul></td>";
    459     for (const auto &Measurement : Points[PointId].Measurements) {
    460       OS << "<td class=\"measurement\">";
    461       writeMeasurementValue<kEscapeHtml>(OS, Measurement.PerInstructionValue);
    462     }
    463     OS << "</tr>";
    464   }
    465   OS << "</table>";
    466 
    467   OS << "</div>";
    468 
    469 } // namespace exegesis
    470 
    471 static constexpr const char kHtmlHead[] = R"(
    472 <head>
    473 <title>llvm-exegesis Analysis Results</title>
    474 <style>
    475 body {
    476   font-family: sans-serif
    477 }
    478 span.sched-class-name {
    479   font-weight: bold;
    480   font-family: monospace;
    481 }
    482 span.opcode {
    483   font-family: monospace;
    484 }
    485 span.config {
    486   font-family: monospace;
    487 }
    488 div.inconsistency {
    489   margin-top: 50px;
    490 }
    491 table {
    492   margin-left: 50px;
    493   border-collapse: collapse;
    494 }
    495 table, table tr,td,th {
    496   border: 1px solid #444;
    497 }
    498 table ul {
    499   padding-left: 0px;
    500   margin: 0px;
    501   list-style-type: none;
    502 }
    503 table.sched-class-clusters td {
    504   padding-left: 10px;
    505   padding-right: 10px;
    506   padding-top: 10px;
    507   padding-bottom: 10px;
    508 }
    509 table.sched-class-desc td {
    510   padding-left: 10px;
    511   padding-right: 10px;
    512   padding-top: 2px;
    513   padding-bottom: 2px;
    514 }
    515 span.mono {
    516   font-family: monospace;
    517 }
    518 td.measurement {
    519   text-align: center;
    520 }
    521 tr.good-cluster td.measurement {
    522   color: #292
    523 }
    524 tr.bad-cluster td.measurement {
    525   color: #922
    526 }
    527 tr.good-cluster td.measurement span.minmax {
    528   color: #888;
    529 }
    530 tr.bad-cluster td.measurement span.minmax {
    531   color: #888;
    532 }
    533 </style>
    534 </head>
    535 )";
    536 
    537 template <>
    538 Error Analysis::run<Analysis::PrintSchedClassInconsistencies>(
    539     raw_ostream &OS) const {
    540   const auto &FirstPoint = Clustering_.getPoints()[0];
    541   // Print the header.
    542   OS << "<!DOCTYPE html><html>" << kHtmlHead << "<body>";
    543   OS << "<h1><span class=\"mono\">llvm-exegesis</span> Analysis Results</h1>";
    544   OS << "<h3>Triple: <span class=\"mono\">";
    545   writeEscaped<kEscapeHtml>(OS, FirstPoint.LLVMTriple);
    546   OS << "</span></h3><h3>Cpu: <span class=\"mono\">";
    547   writeEscaped<kEscapeHtml>(OS, FirstPoint.CpuName);
    548   OS << "</span></h3>";
    549 
    550   for (const auto &RSCAndPoints : makePointsPerSchedClass()) {
    551     if (!RSCAndPoints.RSC.SCDesc)
    552       continue;
    553     // Bucket sched class points into sched class clusters.
    554     std::vector<SchedClassCluster> SchedClassClusters;
    555     for (const size_t PointId : RSCAndPoints.PointIds) {
    556       const auto &ClusterId = Clustering_.getClusterIdForPoint(PointId);
    557       if (!ClusterId.isValid())
    558         continue; // Ignore noise and errors. FIXME: take noise into account ?
    559       if (ClusterId.isUnstable() ^ AnalysisDisplayUnstableOpcodes_)
    560         continue; // Either display stable or unstable clusters only.
    561       auto SchedClassClusterIt = llvm::find_if(
    562           SchedClassClusters, [ClusterId](const SchedClassCluster &C) {
    563             return C.id() == ClusterId;
    564           });
    565       if (SchedClassClusterIt == SchedClassClusters.end()) {
    566         SchedClassClusters.emplace_back();
    567         SchedClassClusterIt = std::prev(SchedClassClusters.end());
    568       }
    569       SchedClassClusterIt->addPoint(PointId, Clustering_);
    570     }
    571 
    572     // Print any scheduling class that has at least one cluster that does not
    573     // match the checked-in data.
    574     if (all_of(SchedClassClusters, [this,
    575                                     &RSCAndPoints](const SchedClassCluster &C) {
    576           return C.measurementsMatch(*SubtargetInfo_, RSCAndPoints.RSC,
    577                                      Clustering_,
    578                                      AnalysisInconsistencyEpsilonSquared_);
    579         }))
    580       continue; // Nothing weird.
    581 
    582     OS << "<div class=\"inconsistency\"><p>Sched Class <span "
    583           "class=\"sched-class-name\">";
    584 #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
    585     writeEscaped<kEscapeHtml>(OS, RSCAndPoints.RSC.SCDesc->Name);
    586 #else
    587     OS << RSCAndPoints.RSC.SchedClassId;
    588 #endif
    589     OS << "</span> contains instructions whose performance characteristics do"
    590           " not match that of LLVM:</p>";
    591     printSchedClassClustersHtml(SchedClassClusters, RSCAndPoints.RSC, OS);
    592     OS << "<p>llvm SchedModel data:</p>";
    593     printSchedClassDescHtml(RSCAndPoints.RSC, OS);
    594     OS << "</div>";
    595   }
    596 
    597   printClusterRawHtml(InstructionBenchmarkClustering::ClusterId::noise(),
    598                       "[noise]", OS);
    599 
    600   OS << "</body></html>";
    601   return Error::success();
    602 }
    603 
    604 } // namespace exegesis
    605 } // namespace llvm
    606