LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
line_console.h
Go to the documentation of this file.
1#pragma once
2
3/**
4 * @file line_console.h
5 * @ingroup line_util
6 * @brief Running progress log of a LINE solver run (the "solver console").
7 *
8 * The console narrates what a solver is doing while it does it: reading the
9 * model, compiling the network structure, computing chains and demands,
10 * resolving the method, iterating, and closing with the figures of merit. Each
11 * line carries the elapsed time since the run started:
12 *
13 * @code
14 * [ 0.014s] compiling the network structure of model 'cqn'
15 * [ 0.031s] computing the routing table and the chains
16 * [ 0.052s] recognized a closed queueing network: 3 stations, 1 class, 1 chain
17 * [ 0.061s] AMVA sweep 10: queue-length residual 1.11e-01, X = 1.2225
18 * @endcode
19 *
20 * It prints no tables: the result table stays the caller's own printer. THE
21 * CONSOLE IS VerboseLevel::DEBUG: it narrates exactly when the run is at DEBUG
22 * and is silent at every lower level, and it never alters a numerical result.
23 * There is no separate console switch -- the console was one, until it became
24 * clear that a running progress log IS what a debug verbosity is for, and two
25 * switches for one channel only let a session ask for DEBUG and get nothing.
26 * line-cli asks for it with `-v debug`; a library caller with set_verbose().
27 *
28 * Nested runs (an inner solver driven by an ensemble) do not narrate: only the
29 * outermost run writes. Use is_active() to suppress a legacy print, owns_log()
30 * to emit.
31 *
32 * Mirrors MATLAB matlab/src/io/LineConsole.m, jline.io.LineConsole and python
33 * line_solver/api/io/console.py.
34 */
35
36#include <algorithm>
37#include <cctype>
38#include <chrono>
39#include <cstdarg>
40#include <cstdio>
41#include <string>
42#include <vector>
43
44namespace line {
45namespace util {
46
47/**
48 * Session verbosity, the twin of MATLAB's VerboseLevel, jline.VerboseLevel and
49 * python's line_solver.constants.VerboseLevel. The ordering matters and matches
50 * theirs: SILENT < STD < DEBUG.
51 *
52 * This port had none until the console became DEBUG. A per-run `bool verbose`
53 * still travels with each solver's options and still means STD-or-SILENT;
54 * DEBUG is a SESSION level, because the console must also narrate the model
55 * compile, which happens before any solver options exist.
56 */
57enum class VerboseLevel { SILENT = 0, STD = 1, DEBUG = 2 };
58
60public:
61 // ---------------------------------------------------------------- state
62
63 /** Set the session verbosity. DEBUG is what switches the console on. */
64 static void set_verbose(VerboseLevel level) {
65 state().verbose = level;
66 reset();
67 }
68
69 /** The session verbosity. */
70 static VerboseLevel get_verbose() { return state().verbose; }
71
72 /** Forget any open run (used after an interrupted solve). */
73 static void reset() {
74 State& st = state();
75 st.depth = 0;
76 st.active = false;
77 st.tag.clear();
78 st.model_name.clear();
79 st.t0 = clock_type::now();
80 st.tsetup = -1.0;
81 st.quiet = 0;
82 st.muted = 0;
83 st.force_detail = false;
84 st.compiling_own = false;
85 st.header_pending = false;
86 st.last_loop.clear();
87 st.shown = 0;
88 st.trunc = false;
89 st.detail_last.clear();
90 st.detail_shapes.clear();
91 st.detail_shape_count.clear();
92 st.detail_total = 0;
93 }
94
95 /**
96 * Resolve whether a run should narrate.
97 *
98 * THE CONSOLE IS DEBUG, and this is the whole rule: a run narrates when the
99 * session is at DEBUG and at no lower level. The run's own `verbose` flag
100 * still vetoes -- it spells SILENT when false, and a run asked to stay
101 * silent stays silent whatever the session asks for -- but it cannot switch
102 * the console ON, because `true` there means STD, not DEBUG.
103 */
104 static bool wanted(bool verbose = true) {
105 return state().verbose == VerboseLevel::DEBUG && verbose;
106 }
107
108 /** True while a run is narrating; gates SUPPRESSION of legacy prints. */
109 static bool is_active() { return state().active; }
110
111 /** True only inside the OUTERMOST open run; gates EMISSION. */
112 static bool owns_log() {
113 const State& st = state();
114 return st.active && st.depth <= 1 && st.muted == 0;
115 }
116
117 /**
118 * True when a progress line should be printed: either the outermost run is
119 * narrating, or no run is open and the session is at DEBUG -- the second
120 * case is what lets model construction narrate before any solver exists.
121 */
122 static bool writes() {
123 const State& st = state();
124 if (st.muted > 0) return false;
125 if (st.depth > 0) return owns_log();
126 return st.verbose == VerboseLevel::DEBUG;
127 }
128
129 // ------------------------------------------------------------ lifecycle
130
131 /**
132 * Open a console run. Pair with end_run(), or use the Run guard below so
133 * that a failed analysis still reports what it had reached.
134 *
135 * @param tag short solver name, e.g. "MVA"
136 * @param model_name the model under study
137 * @param verbose the run's own verbosity flag
138 */
139 static bool begin_run(const std::string& tag, const std::string& model_name,
140 bool verbose = true) {
141 State& st = state();
142 if (st.depth == 0 && !wanted(verbose)) {
143 // A run that must stay silent MUTES the console for its whole
144 // duration, so nothing it triggers leaks out at depth 0.
145 st.muted++;
146 return false;
147 }
148 st.depth++;
149 if (st.depth > 1) return false; // nested: the outer analyzer owns the log
150 st.active = true;
151 st.tag = tag;
152 st.model_name = model_name;
153 st.t0 = clock_type::now();
154 st.tsetup = -1.0;
155 st.last_loop.clear();
156 st.shown = 0;
157 st.trunc = false;
158 st.detail_last.clear();
159 st.detail_shapes.clear();
160 st.detail_shape_count.clear();
161 st.detail_total = 0;
162 // The CLI knows the solver method name before it reads the model file, so the
163 // header waits for the model name rather than printing an empty one; it
164 // is emitted by the first compile, or at close if none happens.
165 if (model_name.empty()) {
166 st.header_pending = true;
167 } else {
168 emit_header();
169 }
170 return true;
171 }
172
173 /** Mark the end of the setup phase, so the closing line can split the time. */
174 static void setup_done() {
175 State& st = state();
176 if (st.active && st.depth == 1) st.tsetup = elapsed(st.t0);
177 }
178
179 /** Close the innermost open run, writing the closing line. */
180 static void end_run() {
181 State& st = state();
182 if (st.depth <= 0) {
183 if (st.muted > 0) st.muted--;
184 return;
185 }
186 st.depth--;
187 if (st.depth > 0 || !st.active) return;
188 if (st.header_pending) {
189 st.header_pending = false;
190 emit_header(false); // a header deferred to the close keeps the clock
191 }
192 const double total = elapsed(st.t0);
193 if (st.tsetup < 0.0) {
194 step("DONE in %.4f s", total);
195 } else {
196 step("DONE in %.4f s (setup %.4f s, analysis %.4f s)", total, st.tsetup,
197 std::max(0.0, total - st.tsetup));
198 }
199 // the mute of an enclosing silenced run outlives this run's own state
200 const int outer_mute = st.muted;
201 reset();
202 state().muted = outer_mute;
203 }
204
205 /** RAII guard: opens a run on construction, closes it on scope exit. */
206 class Run {
207 public:
208 Run(const std::string& tag, const std::string& model_name, bool verbose = true) {
209 begin_run(tag, model_name, verbose);
210 }
211 ~Run() { end_run(); }
212 Run(const Run&) = delete;
213 Run& operator=(const Run&) = delete;
214 };
215
216 /** RAII guard that suppresses structure-compile detail while it lives. */
217 class Quiet {
218 public:
219 Quiet() { state().quiet++; }
220 ~Quiet() { state().quiet = std::max(0, state().quiet - 1); }
221 Quiet(const Quiet&) = delete;
222 Quiet& operator=(const Quiet&) = delete;
223 };
224
225 // ------------------------------------------------------------- emission
226
227 /** Write one progress line. */
228 static void step(const char* fmt, ...) {
229 if (!writes()) return;
230 va_list args;
231 va_start(args, fmt);
232 const std::string text = vformat(fmt, args);
233 va_end(args);
234 emit("", text);
235 }
236
237 /** Write one indented progress line. */
238 static void substep(const char* fmt, ...) {
239 if (!writes()) return;
240 va_list args;
241 va_start(args, fmt);
242 const std::string text = vformat(fmt, args);
243 va_end(args);
244 emit(" ", text);
245 }
246
247 /**
248 * One stage line of a structure compile: silenced inside a Quiet scope,
249 * and inside an open run, where the structures compiled are those of
250 * auxiliary models rather than of the model under study.
251 */
252 static void compile_detail(const char* fmt, ...) {
253 const State& st = state();
254 if (st.quiet > 0) return;
255 if (st.depth > 0 && !st.force_detail && !st.compiling_own) return;
256 if (!writes()) return;
257 va_list args;
258 va_start(args, fmt);
259 const std::string text = vformat(fmt, args);
260 va_end(args);
261 emit(" ", text);
262 }
263
264 /**
265 * Announce the compilation of a model structure.
266 *
267 * A run opened before the model file was read carries no model name yet
268 * (the CLI knows the solver method name first), so the FIRST compile inside such
269 * a run names the run: it is the model under study, not an auxiliary one.
270 */
271 static void compiling(const std::string& name) {
272 State& st = state();
273 if (st.depth > 0 && st.model_name.empty()) {
274 st.model_name = name;
275 if (st.header_pending) {
276 st.header_pending = false;
277 emit_header();
278 }
279 }
280 if (st.depth > 0 && name != st.model_name) {
281 // an ensemble rebuilds the same submodel once per stage, so these
282 // go through detail() and collapse to one line
283 st.compiling_own = false;
284 detail("refreshing the auxiliary submodel '" + name + "'");
285 } else {
286 st.compiling_own = true;
287 if (st.depth == 0) { // a compile outside any run opens its own timeline
288 st.clock = clock_type::now();
289 st.clock_started = true;
290 }
291 step("compiling the network structure of model '%s'", name.c_str());
292 }
293 }
294
295 /**
296 * Report a solver's own debug message as a substep. Consecutive repeats are
297 * dropped, at most three messages of the same SHAPE (the text with its
298 * numbers masked) are reported, and the channel is capped per run.
299 */
300 static void detail(const std::string& raw) {
301 if (!owns_log()) return;
302 const std::string text = trim(raw);
303 State& st = state();
304 if (text.empty() || text == st.detail_last) return;
305 const std::string shape = mask_numbers(text);
306 for (std::size_t i = 0; i < st.detail_shapes.size(); ++i) {
307 if (st.detail_shapes[i] == shape) {
308 if (++st.detail_shape_count[i] > kMaxPerShape) return;
309 st.detail_last = text;
310 bump_and_emit(text);
311 return;
312 }
313 }
314 st.detail_shapes.push_back(shape);
315 st.detail_shape_count.push_back(1);
316 st.detail_last = text;
317 bump_and_emit(text);
318 }
319
320 /**
321 * Announce an iteration loop and reset its reporting budget. Re-announcing
322 * the SAME text (a solver that restarts its loop) neither reprints the
323 * header nor refills the budget.
324 */
325 static void loop(const char* fmt, ...) {
326 if (!owns_log()) return;
327 va_list args;
328 va_start(args, fmt);
329 const std::string text = vformat(fmt, args);
330 va_end(args);
331 State& st = state();
332 if (text == st.last_loop) return;
333 st.last_loop = text;
334 st.shown = 0;
335 st.trunc = false;
336 emit("", text);
337 }
338
339 /**
340 * Report iteration @p k of the current loop. The first 20 iterations report
341 * in full, then every 10th, and the loop stops after 30 lines so a long run
342 * cannot bury the rest of the narration.
343 */
344 static void iter(long k, const char* fmt, ...) {
345 if (!owns_log()) return;
346 if (k > 20 && (k % 10) != 0) return;
347 State& st = state();
348 if (st.shown >= kMaxIterLines) {
349 if (!st.trunc) {
350 st.trunc = true;
351 emit(" ", "further iterations of this loop not reported");
352 }
353 return;
354 }
355 va_list args;
356 va_start(args, fmt);
357 const std::string text = vformat(fmt, args);
358 va_end(args);
359 st.shown++;
360 emit(" ", text);
361 }
362
363 /** Count with an agreeing noun, e.g. "1 chain" or "3 chains". */
364 static std::string plural(long n, const std::string& singular,
365 const std::string& plural_form) {
366 char buf[64];
367 std::snprintf(buf, sizeof(buf), "%ld ", n);
368 return std::string(buf) + (n == 1 ? singular : plural_form);
369 }
370
371 /** Lift the compile detail rule while the run compiles its OWN model. */
373 public:
374 OwnModelCompile() { state().force_detail = true; }
375 ~OwnModelCompile() { state().force_detail = false; }
378 };
379
380private:
381 typedef std::chrono::steady_clock clock_type;
382
383 static const int kMaxIterLines = 30;
384 static const int kMaxDetailLines = 200;
385 static const int kMaxPerShape = 3;
386
387 struct State {
389 int depth = 0;
390 bool active = false;
391 std::string tag;
392 std::string model_name;
393 clock_type::time_point t0 = clock_type::now();
394 double tsetup = -1.0;
395 int quiet = 0;
396 int muted = 0;
397 bool force_detail = false;
398 bool compiling_own = false;
399 bool header_pending = false;
400 std::string last_loop;
401 int shown = 0;
402 bool trunc = false;
403 std::string detail_last;
404 std::vector<std::string> detail_shapes;
405 std::vector<int> detail_shape_count;
406 int detail_total = 0;
407 clock_type::time_point clock = clock_type::now(); // restarted per run
408 bool clock_started = false;
409 };
410
411 static State& state() {
412 static State st;
413 return st;
414 }
415
416 static void emit_header(bool restart_clock = true) {
417 State& st = state();
418 if (restart_clock) { // each run's timeline starts at zero
419 st.clock = clock_type::now();
420 st.clock_started = true;
421 }
422 if (writes()) { // the opening row is set off from whatever preceded it
423 std::printf("\n");
424 }
425 step("LINE: Solver%s starting on model '%s' (lang cpp)", st.tag.c_str(),
426 st.model_name.empty() ? "(unnamed)" : st.model_name.c_str());
427 }
428
429 static double elapsed(const clock_type::time_point& from) {
430 return std::chrono::duration<double>(clock_type::now() - from).count();
431 }
432
433 static void emit(const char* indent, const std::string& text) {
434 State& st = state();
435 if (!st.clock_started) {
436 st.clock = clock_type::now();
437 st.clock_started = true;
438 }
439 // a top-level row opens with a capital, an indented substep stays lowercase
440 std::string row = text;
441 if (indent[0] == '\0' && !row.empty()) {
442 row[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(row[0])));
443 }
444 std::printf("[%8.3fs] %s%s\n", elapsed(st.clock), indent, row.c_str());
445 std::fflush(stdout);
446 }
447
448 static void bump_and_emit(const std::string& text) {
449 State& st = state();
450 if (++st.detail_total > kMaxDetailLines) {
451 if (st.detail_total == kMaxDetailLines + 1)
452 emit(" ", "further solver detail not reported");
453 return;
454 }
455 emit(" ", lower_first(text));
456 }
457
458 static std::string vformat(const char* fmt, va_list args) {
459 va_list copy;
460 va_copy(copy, args);
461 const int n = std::vsnprintf(nullptr, 0, fmt, copy);
462 va_end(copy);
463 if (n <= 0) return std::string(fmt);
464 std::vector<char> buf(static_cast<std::size_t>(n) + 1);
465 std::vsnprintf(buf.data(), buf.size(), fmt, args);
466 return std::string(buf.data(), static_cast<std::size_t>(n));
467 }
468
469 static std::string trim(const std::string& s) {
470 std::size_t b = s.find_first_not_of(" \t\r\n");
471 if (b == std::string::npos) return std::string();
472 std::size_t e = s.find_last_not_of(" \t\r\n");
473 return s.substr(b, e - b + 1);
474 }
475
476 /** The text with every number replaced by '#', so repeats collapse. */
477 static std::string mask_numbers(const std::string& s) {
478 std::string out;
479 out.reserve(s.size());
480 bool in_number = false;
481 for (std::size_t i = 0; i < s.size(); ++i) {
482 const char c = s[i];
483 const bool digit = (c >= '0' && c <= '9');
484 const bool part = digit || (in_number && (c == '.' || c == 'e' || c == 'E' ||
485 ((c == '+' || c == '-') && i > 0 &&
486 (s[i - 1] == 'e' || s[i - 1] == 'E'))));
487 if (part) {
488 if (!in_number) {
489 out.push_back('#');
490 in_number = true;
491 }
492 } else {
493 in_number = false;
494 out.push_back(c);
495 }
496 }
497 return out;
498 }
499
500 static std::string lower_first(const std::string& s) {
501 if (s.size() >= 2) {
502 const bool acronym = (s[0] >= 'A' && s[0] <= 'Z') && (s[1] >= 'A' && s[1] <= 'Z');
503 if (!acronym && s[0] >= 'A' && s[0] <= 'Z') {
504 std::string out = s;
505 out[0] = static_cast<char>(s[0] - 'A' + 'a');
506 return out;
507 }
508 }
509 return s;
510 }
511};
512
513} // namespace util
514} // namespace line
OwnModelCompile(const OwnModelCompile &)=delete
OwnModelCompile & operator=(const OwnModelCompile &)=delete
Quiet(const Quiet &)=delete
Quiet & operator=(const Quiet &)=delete
Run(const Run &)=delete
Run & operator=(const Run &)=delete
Run(const std::string &tag, const std::string &model_name, bool verbose=true)
static void reset()
Forget any open run (used after an interrupted solve).
static void set_verbose(VerboseLevel level)
Set the session verbosity.
static std::string plural(long n, const std::string &singular, const std::string &plural_form)
Count with an agreeing noun, e.g.
static void loop(const char *fmt,...)
Announce an iteration loop and reset its reporting budget.
static void substep(const char *fmt,...)
Write one indented progress line.
static VerboseLevel get_verbose()
The session verbosity.
static bool is_active()
True while a run is narrating; gates SUPPRESSION of legacy prints.
static bool wanted(bool verbose=true)
Resolve whether a run should narrate.
static void compiling(const std::string &name)
Announce the compilation of a model structure.
static void end_run()
Close the innermost open run, writing the closing line.
static void iter(long k, const char *fmt,...)
Report iteration k of the current loop.
static bool owns_log()
True only inside the OUTERMOST open run; gates EMISSION.
static void step(const char *fmt,...)
Write one progress line.
static bool writes()
True when a progress line should be printed: either the outermost run is narrating,...
static void setup_done()
Mark the end of the setup phase, so the closing line can split the time.
static bool begin_run(const std::string &tag, const std::string &model_name, bool verbose=true)
Open a console run.
static void compile_detail(const char *fmt,...)
One stage line of a structure compile: silenced inside a Quiet scope, and inside an open run,...
std::string trim(const std::string &s)
Trims ASCII whitespace from both ends, as Java's String.trim() does.
Definition subprocess.h:181
VerboseLevel
Session verbosity, the twin of MATLAB's VerboseLevel, jline.VerboseLevel and python's line_solver....