Index: include/llvm/Support/Progress.h =================================================================== --- /dev/null +++ include/llvm/Support/Progress.h @@ -0,0 +1,96 @@ +//===--- Progress.h - ANSI Terminal Progress Bar ----------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file declares an ANSI Terminal Progress Bar. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_SUPPORT_PROGRESS_H +#define LLVM_SUPPORT_PROGRESS_H + +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include + +namespace llvm { +/// A 3-line progress bar, which looks like: +/// +/// \verbatim +/// +/// Header +/// 20% [===========----------------------------------] +/// progress message +/// +/// \endverbatim +/// +/// This is modeled heavily off of the one in LIT. +class ProgressBar { + // Colors purposefully chosen to be *different* than LIT's so as not to be + // confusing. For just about everything else, we try to match the behavior + // pretty closely. + static const auto HeaderColor = llvm::raw_ostream::YELLOW; + static const auto BarColor = llvm::raw_ostream::MAGENTA; + static const unsigned RefreshRate = 50; //< Max refresh rate, in milliseconds. +public: + ProgressBar(llvm::raw_ostream &OS, llvm::StringRef Header, bool ETA=true) + : OS(OS), Header(Header), ShowETA(ETA) {} + + /// Update the amount of progress. + /// + /// Causes the bar to be Visible if it wasn't already. + /// + /// \param Percent A value in the range [0.0f, 1.0f] indicating the + /// amount of progress to display. + /// \param Msg The message to display underneath the bar. + void update(float Percent, llvm::StringRef Msg); + + /// Remove the progress bar from the terminal. + void clear(); + +protected: + + /// Special case for when the ProgressBar itself is under test + void markUnderTest() { UnderTest = true; } + +private: + + /// The stream being written to. + llvm::raw_ostream &OS; + + /// Header text to print centered at the top of the bar. + std::string Header; + + /// Whether to make a guess at estimated completion. + bool ShowETA; + + /// If `ShowETA && Visible`, contains the time when the progress bar was + /// first displayed. + std::chrono::time_point Start; + + /// Contains the time when the progress bar was last displayed. Used to + /// rate-limit the refresh, to avoid flickering. + llvm::Optional> LastUpdate; + + /// Is the progress bar being displayed? + bool Visible = false; + + /// Total number of characters written out on the last line. + unsigned Width = 0; + + /// For testing purposes + bool UnderTest = false; +}; + +} // namespace llvm + +#endif + Index: lib/Support/CMakeLists.txt =================================================================== --- lib/Support/CMakeLists.txt +++ lib/Support/CMakeLists.txt @@ -86,6 +86,7 @@ Parallel.cpp PluginLoader.cpp PrettyStackTrace.cpp + Progress.cpp RandomNumberGenerator.cpp Regex.cpp ScaledNumber.cpp Index: lib/Support/Progress.cpp =================================================================== --- /dev/null +++ lib/Support/Progress.cpp @@ -0,0 +1,117 @@ +//===--- lib/Support/Progress.cpp - ANSI Terminal Progress Bar --*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file defines an ANSI Terminal Progress Bar. +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/Progress.h" + +#include "llvm/Support/Compiler.h" +#include "llvm/Support/Process.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Support/Format.h" + +#include + +using namespace llvm; + +void ProgressBar::update(float Percent, StringRef Msg) { + if (!OS.is_displayed() && !LLVM_UNLIKELY(UnderTest)) + return; + + std::string ETAStr; + if (ShowETA) { + if (!Visible) + Start = std::chrono::system_clock::now(); + auto Elapsed = std::chrono::duration_cast( + std::chrono::system_clock::now() - Start).count(); + if (Percent > 0.0001 && Elapsed > 1) { + auto Total = Elapsed / Percent; + auto ETA = unsigned(Total - Elapsed); + unsigned HH = ETA / 3600; + unsigned MM = (ETA / 60) % 60; + unsigned SS = ETA % 60; + raw_string_ostream RSO(ETAStr); + RSO << format(" ETA %02d:%02d:%02d", HH, MM, SS); + ETAStr = RSO.str(); + } + } + + if (LastUpdate && + std::chrono::duration_cast( + std::chrono::system_clock::now() - *LastUpdate).count() < + RefreshRate && !LLVM_UNLIKELY(UnderTest)) + return; + + clear(); + + unsigned TotalWidth = UnderTest ? 60 : sys::Process::StandardOutColumns(); + unsigned PBWidth = TotalWidth - (1 + 4 + 2 + 1 + ETAStr.size() + 1); + unsigned PBDone = PBWidth * Percent; + + // Centered header + (OS.indent((TotalWidth - Header.size()) / 2) + .changeColor(HeaderColor, /*Bold=*/true) + << Header).resetColor() << "\n"; + + // Progress bar + OS << format(" %3d%% ", unsigned(100 * Percent)); + (OS.changeColor(BarColor) << "[").resetColor(); + (OS.changeColor(BarColor, /*Bold=*/true) + << std::string(PBDone, '=') << std::string(PBWidth - PBDone, '-')) + .resetColor(); + (OS.changeColor(BarColor) << "]").resetColor(); + OS << ETAStr << "\n"; + + // Footer messaage + if (Msg.size() < TotalWidth) { + OS << Msg; + Width = Msg.size(); + } else { + OS << Msg.substr(0, TotalWidth - 4) << "... "; + Width = TotalWidth; + } + + // Hide the cursor + OS << "\033[?25l"; + + OS.flush(); + Visible = true; + LastUpdate = std::chrono::system_clock::now(); +} + +void ProgressBar::clear() { + if (!LLVM_UNLIKELY(UnderTest)) { + if (!Visible) + return; + + if (!OS.is_displayed()) + return; + } + + // Move to beginning of current the line + OS << "\033[" << (Width + 1) << "D" + + // Clear it + << "\033[2K" + + // Move up a line and clear it + << "\033[1A\033[2K" + + // Move up a line and clear it + << "\033[1A\033[2K" + + // Unhide the cursor + << "\033[?25h"; + + OS.flush(); + Visible = false; +} + Index: unittests/Support/CMakeLists.txt =================================================================== --- unittests/Support/CMakeLists.txt +++ unittests/Support/CMakeLists.txt @@ -41,6 +41,7 @@ Path.cpp ProcessTest.cpp ProgramTest.cpp + ProgressTest.cpp RegexTest.cpp ReverseIterationTest.cpp ReplaceFileTest.cpp Index: unittests/Support/ProgressTest.cpp =================================================================== --- /dev/null +++ unittests/Support/ProgressTest.cpp @@ -0,0 +1,77 @@ +//===----- unittests/Support/Progress.cpp - Progress Bar tests ------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "llvm/Support/Progress.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Config/config.h" + +#include "gtest/gtest.h" +#include +#include + +using namespace llvm; + +namespace { + +struct TestOutput { + TestOutput() : TOS(Output) {} + std::string Output; + raw_string_ostream TOS; +}; + +class ProgressBarTest + : public ::testing::Test, + // Gross multiple inheritance so we can guarantee that OS is constructed + // before the ProgressBar itself. + public TestOutput, + public ProgressBar { +protected: + ProgressBarTest() + : ::testing::Test(), ProgressBar(TOS, "Header", false) {} +}; + +TEST_F(ProgressBarTest, Update) { + // Force 60 columns, among other things. + markUnderTest(); + + update(0.25, "Quarter"); + + // Don't use EXPECT_EQ because, if it fails, LIT will struggle when all these + // ANSI control characters get printed out. + EXPECT_TRUE(TOS.str() == + "\x1B[1D\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K\x1B[?25h" + " Header\n" + " 25% [============---------------------------------------]\n" + "Quarter\x1B[?25l"); + + update(0.75, "Three Quarter"); + + EXPECT_TRUE(TOS.str() == + "\x1B[1D\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K\x1B[?25h" + " Header\n" + " 25% [============---------------------------------------]\n" + "Quarter\x1B[?25l\x1B[8D\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K\x1B[?25h" + " Header\n" + " 75% [======================================-------------]\n" + "Three Quarter\x1B[?25l"); + + clear(); + + EXPECT_TRUE(TOS.str() == + "\x1B[1D\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K\x1B[?25h" + " Header\n" + " 25% [============---------------------------------------]\n" + "Quarter\x1B[?25l\x1B[8D\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K\x1B[?25h" + " Header\n" + " 75% [======================================-------------]\n" + "Three Quarter\x1B[?25l\x1B[14D\x1B[2K\x1B[1A\x1B[2K\x1B[1A\x1B[2K\x1B[?25h"); + +} + +} // end anon namespace