/src/gdal/port/cpl_progress.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: CPL - Common Portability Library |
4 | | * Author: Frank Warmerdam, warmerdam@pobox.com |
5 | | * Purpose: Progress function implementations. |
6 | | * |
7 | | ****************************************************************************** |
8 | | * Copyright (c) 2013, Frank Warmerdam |
9 | | * |
10 | | * SPDX-License-Identifier: MIT |
11 | | ****************************************************************************/ |
12 | | |
13 | | #include "cpl_progress.h" |
14 | | |
15 | | #include <cmath> |
16 | | #include <cstdio> |
17 | | #include <ctime> |
18 | | |
19 | | #include <algorithm> |
20 | | |
21 | | #include "cpl_conv.h" |
22 | | #include "cpl_string.h" |
23 | | |
24 | | /************************************************************************/ |
25 | | /* GDALDummyProgress() */ |
26 | | /************************************************************************/ |
27 | | |
28 | | /** |
29 | | * \brief Stub progress function. |
30 | | * |
31 | | * This is a stub (does nothing) implementation of the GDALProgressFunc() |
32 | | * semantics. It is primarily useful for passing to functions that take |
33 | | * a GDALProgressFunc() argument but for which the application does not want |
34 | | * to use one of the other progress functions that actually do something. |
35 | | */ |
36 | | |
37 | | int CPL_STDCALL GDALDummyProgress(double /* dfComplete */, |
38 | | const char * /* pszMessage */, |
39 | | void * /* pData */) |
40 | 0 | { |
41 | 0 | return TRUE; |
42 | 0 | } |
43 | | |
44 | | /************************************************************************/ |
45 | | /* GDALScaledProgress() */ |
46 | | /************************************************************************/ |
47 | | typedef struct |
48 | | { |
49 | | GDALProgressFunc pfnProgress; |
50 | | void *pData; |
51 | | double dfMin; |
52 | | double dfMax; |
53 | | } GDALScaledProgressInfo; |
54 | | |
55 | | /** |
56 | | * \brief Scaled progress transformer. |
57 | | * |
58 | | * This is the progress function that should be passed along with the |
59 | | * callback data returned by GDALCreateScaledProgress(). |
60 | | */ |
61 | | |
62 | | int CPL_STDCALL GDALScaledProgress(double dfComplete, const char *pszMessage, |
63 | | void *pData) |
64 | | |
65 | 0 | { |
66 | 0 | GDALScaledProgressInfo *psInfo = |
67 | 0 | reinterpret_cast<GDALScaledProgressInfo *>(pData); |
68 | | |
69 | | // Optimization if GDALCreateScaledProgress() provided with |
70 | | // GDALDummyProgress. |
71 | 0 | if (psInfo == nullptr) |
72 | 0 | return TRUE; |
73 | | |
74 | 0 | return psInfo->pfnProgress(dfComplete * (psInfo->dfMax - psInfo->dfMin) + |
75 | 0 | psInfo->dfMin, |
76 | 0 | pszMessage, psInfo->pData); |
77 | 0 | } |
78 | | |
79 | | /************************************************************************/ |
80 | | /* GDALCreateScaledProgress() */ |
81 | | /************************************************************************/ |
82 | | |
83 | | /** |
84 | | * \brief Create scaled progress transformer. |
85 | | * |
86 | | * Sometimes when an operations wants to report progress it actually |
87 | | * invokes several subprocesses which also take GDALProgressFunc()s, |
88 | | * and it is desirable to map the progress of each sub operation into |
89 | | * a portion of 0.0 to 1.0 progress of the overall process. The scaled |
90 | | * progress function can be used for this. |
91 | | * |
92 | | * For each subsection a scaled progress function is created and |
93 | | * instead of passing the overall progress func down to the sub functions, |
94 | | * the GDALScaledProgress() function is passed instead. |
95 | | * |
96 | | * @param dfMin the value to which 0.0 in the sub operation is mapped. |
97 | | * @param dfMax the value to which 1.0 is the sub operation is mapped. |
98 | | * @param pfnProgress the overall progress function. |
99 | | * @param pData the overall progress function callback data. |
100 | | * |
101 | | * @return pointer to pass as pProgressArg to sub functions. Should be freed |
102 | | * with GDALDestroyScaledProgress(). |
103 | | * |
104 | | * Example: |
105 | | * |
106 | | * \code |
107 | | * int MyOperation( ..., GDALProgressFunc pfnProgress, void *pProgressData ); |
108 | | * |
109 | | * { |
110 | | * void *pScaledProgress; |
111 | | * |
112 | | * pScaledProgress = GDALCreateScaledProgress( 0.0, 0.5, pfnProgress, |
113 | | * pProgressData ); |
114 | | * GDALDoLongSlowOperation( ..., GDALScaledProgress, pScaledProgress ); |
115 | | * GDALDestroyScaledProgress( pScaledProgress ); |
116 | | * |
117 | | * pScaledProgress = GDALCreateScaledProgress( 0.5, 1.0, pfnProgress, |
118 | | * pProgressData ); |
119 | | * GDALDoAnotherOperation( ..., GDALScaledProgress, pScaledProgress ); |
120 | | * GDALDestroyScaledProgress( pScaledProgress ); |
121 | | * |
122 | | * return ...; |
123 | | * } |
124 | | * \endcode |
125 | | */ |
126 | | |
127 | | void *CPL_STDCALL GDALCreateScaledProgress(double dfMin, double dfMax, |
128 | | GDALProgressFunc pfnProgress, |
129 | | void *pData) |
130 | | |
131 | 0 | { |
132 | 0 | if (pfnProgress == nullptr || pfnProgress == GDALDummyProgress) |
133 | 0 | return nullptr; |
134 | | |
135 | 0 | GDALScaledProgressInfo *psInfo = static_cast<GDALScaledProgressInfo *>( |
136 | 0 | CPLCalloc(sizeof(GDALScaledProgressInfo), 1)); |
137 | |
|
138 | 0 | if (std::abs(dfMin - dfMax) < 0.0000001) |
139 | 0 | dfMax = dfMin + 0.01; |
140 | |
|
141 | 0 | psInfo->pData = pData; |
142 | 0 | psInfo->pfnProgress = pfnProgress; |
143 | 0 | psInfo->dfMin = dfMin; |
144 | 0 | psInfo->dfMax = dfMax; |
145 | |
|
146 | 0 | return static_cast<void *>(psInfo); |
147 | 0 | } |
148 | | |
149 | | /************************************************************************/ |
150 | | /* GDALDestroyScaledProgress() */ |
151 | | /************************************************************************/ |
152 | | |
153 | | /** |
154 | | * \brief Cleanup scaled progress handle. |
155 | | * |
156 | | * This function cleans up the data associated with a scaled progress function |
157 | | * as returned by GADLCreateScaledProgress(). |
158 | | * |
159 | | * @param pData scaled progress handle returned by GDALCreateScaledProgress(). |
160 | | */ |
161 | | |
162 | | void CPL_STDCALL GDALDestroyScaledProgress(void *pData) |
163 | | |
164 | 0 | { |
165 | 0 | CPLFree(pData); |
166 | 0 | } |
167 | | |
168 | | /************************************************************************/ |
169 | | /* GDALTermProgressWidth() */ |
170 | | /************************************************************************/ |
171 | | |
172 | | static constexpr int GDALTermProgressWidth(int nMaxTicks, int nMajorTickSpacing) |
173 | 0 | { |
174 | 0 | int nWidth = 0; |
175 | 0 | for (int i = 0; i <= nMaxTicks; i++) |
176 | 0 | { |
177 | 0 | if (i % nMajorTickSpacing == 0) |
178 | 0 | { |
179 | 0 | int nPercent = (i * 100) / nMaxTicks; |
180 | 0 | do |
181 | 0 | { |
182 | 0 | nWidth++; |
183 | 0 | } while (nPercent /= 10); |
184 | 0 | } |
185 | 0 | else |
186 | 0 | { |
187 | 0 | nWidth += 1; |
188 | 0 | } |
189 | 0 | } |
190 | 0 | return nWidth; |
191 | 0 | } |
192 | | |
193 | | /************************************************************************/ |
194 | | /* GDALTermProgress() */ |
195 | | /************************************************************************/ |
196 | | |
197 | | /** |
198 | | * \fn GDALTermProgress(double, const char*, void*) |
199 | | * \brief Simple progress report to terminal. |
200 | | * |
201 | | * This progress reporter prints simple progress report to the |
202 | | * terminal window. The progress report generally looks something like |
203 | | * this: |
204 | | |
205 | | \verbatim |
206 | | 0...10...20...30...40...50...60...70...80...90...100 - done. |
207 | | \endverbatim |
208 | | |
209 | | * Starting with GDAL 3.11, for tasks estimated to take more than 10 seconds, |
210 | | * an estimated remaining time is also displayed at the end. And for tasks |
211 | | * taking more than 5 seconds to complete, the total time is displayed upon |
212 | | * completion. |
213 | | * |
214 | | * Every 2.5% of progress another number or period is emitted. Note that |
215 | | * GDALTermProgress() uses internal static data to keep track of the last |
216 | | * percentage reported and will get confused if two terminal based progress |
217 | | * reportings are active at the same time. |
218 | | * |
219 | | * The GDALTermProgress() function maintains an internal memory of the |
220 | | * last percentage complete reported in a static variable, and this makes |
221 | | * it unsuitable to have multiple GDALTermProgress()'s active either in a |
222 | | * single thread or across multiple threads. |
223 | | * |
224 | | * @param dfComplete completion ratio from 0.0 to 1.0. |
225 | | * @param pszMessage optional message. |
226 | | * @param pProgressArg ignored callback data argument. |
227 | | * |
228 | | * @return Always returns TRUE indicating the process should continue. |
229 | | */ |
230 | | |
231 | | int CPL_STDCALL GDALTermProgress(double dfComplete, |
232 | | CPL_UNUSED const char *pszMessage, |
233 | | CPL_UNUSED void *pProgressArg) |
234 | 0 | { |
235 | 0 | constexpr int MAX_TICKS = 40; |
236 | 0 | constexpr int MAJOR_TICK_SPACING = 4; |
237 | 0 | constexpr int LENGTH_OF_0_TO_100_PROGRESS = |
238 | 0 | GDALTermProgressWidth(MAX_TICKS, MAJOR_TICK_SPACING); |
239 | |
|
240 | 0 | const int nThisTick = std::min( |
241 | 0 | MAX_TICKS, std::max(0, static_cast<int>(dfComplete * MAX_TICKS))); |
242 | | |
243 | | // Have we started a new progress run? |
244 | 0 | static int nLastTick = -1; |
245 | 0 | static time_t nStartTime = 0; |
246 | | // whether estimated remaining time is displayed |
247 | 0 | static bool bETADisplayed = false; |
248 | | // number of characters displayed during last progress call |
249 | 0 | static int nCharacterCountLastTime = 0; |
250 | | // maximum number of characters displayed during previous calls |
251 | 0 | static int nCharacterCountMax = 0; |
252 | 0 | if (nThisTick < nLastTick && nLastTick >= MAX_TICKS - 1) |
253 | 0 | { |
254 | 0 | bETADisplayed = false; |
255 | 0 | nLastTick = -1; |
256 | 0 | nCharacterCountLastTime = 0; |
257 | 0 | nCharacterCountMax = 0; |
258 | 0 | } |
259 | |
|
260 | 0 | if (nThisTick <= nLastTick) |
261 | 0 | return TRUE; |
262 | | |
263 | 0 | const time_t nCurTime = time(nullptr); |
264 | 0 | if (nLastTick < 0) |
265 | 0 | nStartTime = nCurTime; |
266 | |
|
267 | 0 | constexpr int MIN_DELAY_FOR_ETA = 5; // in seconds |
268 | 0 | if (nCurTime - nStartTime >= MIN_DELAY_FOR_ETA && dfComplete > 0 && |
269 | 0 | dfComplete < 0.5) |
270 | 0 | { |
271 | 0 | static bool bIsTTY = CPLIsInteractive(stdout); |
272 | 0 | bETADisplayed = bIsTTY; |
273 | 0 | } |
274 | 0 | if (bETADisplayed) |
275 | 0 | { |
276 | 0 | for (int i = 0; i < nCharacterCountLastTime; ++i) |
277 | 0 | fprintf(stdout, "\b"); |
278 | 0 | nLastTick = -1; |
279 | 0 | nCharacterCountLastTime = 0; |
280 | |
|
281 | | #ifdef _WIN32 |
282 | | constexpr const char *WINDOWS_TERMINAL_ENV_VAR = "WT_SESSION"; |
283 | | constexpr const char *CONEMU_ENV_VAR = "ConEmuANSI"; |
284 | | #endif |
285 | 0 | static const bool bAllowOSC94 = CPLTestBool(CPLGetConfigOption( |
286 | 0 | "GDAL_TERM_PROGRESS_OSC_9_4", |
287 | | #ifdef _WIN32 |
288 | | // Detect if we are running under Windows Terminal |
289 | | (CPLGetConfigOption(WINDOWS_TERMINAL_ENV_VAR, nullptr) != nullptr || |
290 | | // or ConEmu |
291 | | CPLGetConfigOption(CONEMU_ENV_VAR, nullptr) != nullptr) |
292 | | ? "YES" |
293 | | : "NO" |
294 | | #else |
295 | 0 | "YES" |
296 | 0 | #endif |
297 | 0 | )); |
298 | 0 | if (bAllowOSC94) |
299 | 0 | { |
300 | | // Implement OSC 9;4 progress reporting protocol |
301 | | // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC |
302 | 0 | if (nThisTick == MAX_TICKS) |
303 | 0 | fprintf(stdout, "\x1b]9;4;0;100\x07"); |
304 | 0 | else |
305 | 0 | fprintf(stdout, "\x1b]9;4;1;%d\x07", |
306 | 0 | (nThisTick * 100) / MAX_TICKS); |
307 | 0 | } |
308 | 0 | } |
309 | |
|
310 | 0 | while (nThisTick > nLastTick) |
311 | 0 | { |
312 | 0 | ++nLastTick; |
313 | 0 | if (nLastTick % MAJOR_TICK_SPACING == 0) |
314 | 0 | { |
315 | 0 | const int nPercent = (nLastTick * 100) / MAX_TICKS; |
316 | 0 | nCharacterCountLastTime += fprintf(stdout, "%d", nPercent); |
317 | 0 | } |
318 | 0 | else |
319 | 0 | { |
320 | 0 | nCharacterCountLastTime += fprintf(stdout, "."); |
321 | 0 | } |
322 | 0 | } |
323 | |
|
324 | 0 | if (nThisTick == MAX_TICKS) |
325 | 0 | { |
326 | 0 | nCharacterCountLastTime += fprintf(stdout, " - done"); |
327 | 0 | if (nCurTime - nStartTime >= MIN_DELAY_FOR_ETA) |
328 | 0 | { |
329 | 0 | const int nElapsed = static_cast<int>(nCurTime - nStartTime); |
330 | 0 | const int nHours = nElapsed / 3600; |
331 | 0 | const int nMins = (nElapsed % 3600) / 60; |
332 | 0 | const int nSecs = nElapsed % 60; |
333 | 0 | nCharacterCountLastTime += |
334 | 0 | fprintf(stdout, " in %02d:%02d:%02d.", nHours, nMins, nSecs); |
335 | 0 | for (int i = nCharacterCountLastTime; i < nCharacterCountMax; ++i) |
336 | 0 | nCharacterCountLastTime += fprintf(stdout, " "); |
337 | 0 | } |
338 | 0 | else |
339 | 0 | { |
340 | 0 | fprintf(stdout, "."); |
341 | 0 | } |
342 | 0 | fprintf(stdout, "\n"); |
343 | 0 | } |
344 | 0 | else |
345 | 0 | { |
346 | 0 | if (bETADisplayed) |
347 | 0 | { |
348 | 0 | for (int i = nCharacterCountLastTime; |
349 | 0 | i < LENGTH_OF_0_TO_100_PROGRESS; ++i) |
350 | 0 | nCharacterCountLastTime += fprintf(stdout, " "); |
351 | |
|
352 | 0 | const double dfETA = |
353 | 0 | (nCurTime - nStartTime) * (1.0 / dfComplete - 1); |
354 | 0 | const int nETA = static_cast<int>(dfETA + 0.5); |
355 | 0 | const int nHours = nETA / 3600; |
356 | 0 | const int nMins = (nETA % 3600) / 60; |
357 | 0 | const int nSecs = nETA % 60; |
358 | 0 | nCharacterCountLastTime += |
359 | 0 | fprintf(stdout, " - estimated remaining time: %02d:%02d:%02d", |
360 | 0 | nHours, nMins, nSecs); |
361 | 0 | for (int i = nCharacterCountLastTime; i < nCharacterCountMax; ++i) |
362 | 0 | nCharacterCountLastTime += fprintf(stdout, " "); |
363 | 0 | } |
364 | 0 | fflush(stdout); |
365 | 0 | } |
366 | |
|
367 | 0 | if (nCharacterCountLastTime > nCharacterCountMax) |
368 | 0 | nCharacterCountMax = nCharacterCountLastTime; |
369 | |
|
370 | 0 | return TRUE; |
371 | 0 | } |