From ae153d0b9df1227ca5eaec068cf7b44f6f6d47a4 Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Mon, 28 Oct 2024 18:28:14 +0100 Subject: [PATCH] GDALTermProgress: display estimated remaining time for tasks >= 10 seconds Refreshed every tick (so 2.5%) (only on interactive terminals. Tested with bash on Linux and cmd on Windows, but should work for all reasonable terminals) ``` Before 5 seconds: 0...10...20... After 5 seconds: 0...10...20...30...40...50 - estimated remaining time: 00:00:07 Upon completion: 0...10...20...30...40...50...60...70...80...90...100 - done in 00:00:13. ``` Fixes #11100 Co-authored-by: Daniel Baston --- port/cpl_progress.cpp | 129 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/port/cpl_progress.cpp b/port/cpl_progress.cpp index 4edf83b187ad..71bb78a597d1 100644 --- a/port/cpl_progress.cpp +++ b/port/cpl_progress.cpp @@ -14,6 +14,7 @@ #include #include +#include #include @@ -163,6 +164,31 @@ void CPL_STDCALL GDALDestroyScaledProgress(void *pData) CPLFree(pData); } +/************************************************************************/ +/* GDALTermProgressWidth() */ +/************************************************************************/ + +static constexpr int GDALTermProgressWidth(int nMaxTicks, int nMajorTickSpacing) +{ + int nWidth = 0; + for (int i = 0; i <= nMaxTicks; i++) + { + if (i % nMajorTickSpacing == 0) + { + int nPercent = (i * 100) / nMaxTicks; + do + { + nWidth++; + } while (nPercent /= 10); + } + else + { + nWidth += 1; + } + } + return nWidth; +} + /************************************************************************/ /* GDALTermProgress() */ /************************************************************************/ @@ -179,6 +205,11 @@ void CPL_STDCALL GDALDestroyScaledProgress(void *pData) 0...10...20...30...40...50...60...70...80...90...100 - done. \endverbatim + * Starting with GDAL 3.11, for tasks estimated to take more than 10 seconds, + * an estimated remaining time is also displayed at the end. And for tasks + * taking more than 5 seconds to complete, the total time is displayed upon + * completion. + * * Every 2.5% of progress another number or period is emitted. Note that * GDALTermProgress() uses internal static data to keep track of the last * percentage reported and will get confused if two terminal based progress @@ -200,30 +231,112 @@ int CPL_STDCALL GDALTermProgress(double dfComplete, CPL_UNUSED const char *pszMessage, CPL_UNUSED void *pProgressArg) { - const int nThisTick = - std::min(40, std::max(0, static_cast(dfComplete * 40.0))); + constexpr int MAX_TICKS = 40; + constexpr int MAJOR_TICK_SPACING = 4; + constexpr int LENGTH_OF_0_TO_100_PROGRESS = + GDALTermProgressWidth(MAX_TICKS, MAJOR_TICK_SPACING); + + const int nThisTick = std::min( + MAX_TICKS, std::max(0, static_cast(dfComplete * MAX_TICKS))); // Have we started a new progress run? static int nLastTick = -1; - if (nThisTick < nLastTick && nLastTick >= 39) + static time_t nStartTime = 0; + // whether estimated remaining time is displayed + static bool bETADisplayed = false; + // number of characters displayed during last progress call + static int nCharacterCountLastTime = 0; + // maximum number of characters displayed during previous calls + static int nCharacterCountMax = 0; + if (nThisTick < nLastTick && nLastTick >= MAX_TICKS - 1) + { + bETADisplayed = false; nLastTick = -1; + nCharacterCountLastTime = 0; + nCharacterCountMax = 0; + } if (nThisTick <= nLastTick) return TRUE; + const time_t nCurTime = time(nullptr); + if (nLastTick < 0) + nStartTime = nCurTime; + + constexpr int MIN_DELAY_FOR_ETA = 5; // in seconds + if (nCurTime - nStartTime >= MIN_DELAY_FOR_ETA && dfComplete > 0 && + dfComplete < 0.5) + { + static bool bIsTTY = CPLIsInteractive(stdout); + bETADisplayed = bIsTTY; + } + if (bETADisplayed) + { + for (int i = 0; i < nCharacterCountLastTime; ++i) + fprintf(stdout, "\b"); + nLastTick = -1; + nCharacterCountLastTime = 0; + } + while (nThisTick > nLastTick) { ++nLastTick; - if (nLastTick % 4 == 0) - fprintf(stdout, "%d", (nLastTick / 4) * 10); + if (nLastTick % MAJOR_TICK_SPACING == 0) + { + const int nPercent = (nLastTick * 100) / MAX_TICKS; + nCharacterCountLastTime += fprintf(stdout, "%d", nPercent); + } else - fprintf(stdout, "."); + { + nCharacterCountLastTime += fprintf(stdout, "."); + } } - if (nThisTick == 40) - fprintf(stdout, " - done.\n"); + if (nThisTick == MAX_TICKS) + { + nCharacterCountLastTime += fprintf(stdout, " - done"); + if (nCurTime - nStartTime >= MIN_DELAY_FOR_ETA) + { + const int nEllapsed = static_cast(nCurTime - nStartTime); + const int nHours = nEllapsed / 3600; + const int nMins = (nEllapsed % 3600) / 60; + const int nSecs = nEllapsed % 60; + nCharacterCountLastTime += + fprintf(stdout, " in %02d:%02d:%02d.", nHours, nMins, nSecs); + for (int i = nCharacterCountLastTime; i < nCharacterCountMax; ++i) + nCharacterCountLastTime += fprintf(stdout, " "); + } + else + { + fprintf(stdout, "."); + } + fprintf(stdout, "\n"); + } else + { + if (bETADisplayed) + { + for (int i = nCharacterCountLastTime; + i < LENGTH_OF_0_TO_100_PROGRESS; ++i) + nCharacterCountLastTime += fprintf(stdout, " "); + + const double dfETA = + (nCurTime - nStartTime) * (1.0 / dfComplete - 1); + const int nETA = static_cast(dfETA + 0.5); + const int nHours = nETA / 3600; + const int nMins = (nETA % 3600) / 60; + const int nSecs = nETA % 60; + nCharacterCountLastTime += + fprintf(stdout, " - estimated remaining time: %02d:%02d:%02d", + nHours, nMins, nSecs); + for (int i = nCharacterCountLastTime; i < nCharacterCountMax; ++i) + nCharacterCountLastTime += fprintf(stdout, " "); + } fflush(stdout); + } + + if (nCharacterCountLastTime > nCharacterCountMax) + nCharacterCountMax = nCharacterCountLastTime; return TRUE; }