LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ldes.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_WRAPPERS_LDES_SOLVER_LDES_H
6#define LINE_SOLVERS_WRAPPERS_LDES_SOLVER_LDES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `SolverLDES`, the discrete-event simulator, as its C++ client.
12 *
13 * WHAT IS PORTED, AND WHY IT IS A CLIENT AND NOT AN ENGINE. LDES has ONE
14 * implementation, `jar/src/main/java/jline/solvers/wrappers/ldes/` (~25 kLOC on SSJ),
15 * and every codebase that is not the JAR reaches it the same way: MATLAB
16 * (`@@SolverLDES/solveCli.m`) and native Python
17 * (`wrappers/solver_ldes/solver_ldes.py`) serialize the model to `model.json`,
18 * run `solve model.json -o result.json` as a subprocess and parse the result
19 * document back. Nothing is marshalled in process. This file is the third such
20 * client, and it is the port of SolverLDES for the same reason those two are:
21 * a second simulator would answer with different numbers under the same name,
22 * and "LDES agrees across the codebases" -- which the parity harness checks and
23 * `CLAUDE.md` states as an invariant -- is a property of there being ONE engine.
24 *
25 * THE PRIMARY ENTRY IS A DOCUMENT, not a model object, and that is a deliberate
26 * difference from the two other clients. They hold a live model and serialize
27 * it; `solver_ldes_text` / `solver_ldes_file` are given the `model.json`
28 * DOCUMENT and forward its bytes UNCHANGED. There is a writer
29 * (`io/network_writer.h`), so a round trip through `qn::NetworkStruct<T>` is
30 * possible -- `solver_ldes` below is exactly that, for a caller who built the
31 * model in C++ -- but the reader that would have to parse the document first is
32 * deliberately scoped to the subset the analytical solvers read, so making the
33 * trip MANDATORY would degrade exactly the models LDES exists for (a cache with
34 * retrieval, an SPN, a polling server, a G-network signal). Forwarding the bytes
35 * keeps the client lossless on every model the engine accepts, including those
36 * this port cannot itself represent.
37 *
38 * WHICH BINARY RUNS. `ldes_runners` reproduces `getLdesRunners` and
39 * `_build_cli_runners`: the AOT GraalVM binary `common/ldes` first, for its
40 * startup time, then `<java> -jar common/ldes.jar`, and each is tried in turn
41 * because the AOT image lacks reflective features the JVM has (the fork-join
42 * MMT transform deep-copies the model by Java serialization). THE ORDER IS
43 * REVERSED, not merely extended, when the run uses something the prebuilt image
44 * predates -- `--respt-samples`, `--initsol`, a cache cost cap, a marked class,
45 * a load/class-dependent scaling -- because on that image the flag is not
46 * refused but IGNORED, and an ignored cost cap silently simulates the uncapped
47 * cache. The two other clients inspect the live model object for those; this
48 * one inspects the document, which is the same information at the point where
49 * it actually crosses.
50 *
51 * STATE PROBABILITIES ARE READ FROM THE HISTOGRAM, not from the trajectory.
52 * `ldes_prob_aggr` / `ldes_prob_sys_aggr` take the target state as an ARGUMENT
53 * and weigh it against `stateHistogram`, the exact residence time of every
54 * integer joint state the run visited. The no-argument form the reference also
55 * offers -- "the probability of the model's CURRENT state" -- has no counterpart
56 * here because `qn::NetworkStruct` carries no current state row (only a Cache's
57 * `initstate`, a Place's `initmarking` and the `stateprior`/`statespace` pair).
58 * The DEFAULT INITIAL state is a different thing and is derivable, by the same
59 * `ctmc::analyzer_detail::default_init_state` the `-a prob` arms of CTMC, MVA
60 * and NC already answer over; the LDES arm of the CLI still refuses `-a prob`
61 * on the older premise that no state exists at all, which is a reachability gap
62 * in `cpp/src/cli/line_cli.cpp` rather than a gap in this client.
63 * The three other clients answer BOTH forms off the transient `QNt` series,
64 * which is a trajectory of interval MEANS, and so report a near-zero
65 * probability for a state the chain mostly occupies; see BUG-96 and the note on
66 * `LdesResult::QNt`.
67 *
68 * WHAT THIS CLIENT DOES NOT PORT, and why it refuses instead of approximating:
69 * LayeredNetwork MATLAB simulates an LQN through the Java ensemble
70 * backend (`self.obj`), an in-process path with no JSON
71 * interface; `LdesCLI` itself refuses a LayeredNetwork
72 * document ("Network models only"). There is nothing to
73 * forward.
74 * server breakdowns `linemodel_save` does not serialize `setBreakdown` at
75 * all, so a breakdown cannot reach this client: the
76 * refusal the Python client performs is on the OBJECT,
77 * before serialization, and has no wire counterpart.
78 *
79 * WHERE THE ENGINE IS, and whether there is one, lives in `ldes_probe.h`:
80 * SolverAUTO needs that answer -- LDES leads several of the reference's
81 * rankings, and a chooser that named it on a host without the engine would be
82 * proposing something that cannot run -- and must not take on this file's HTTP
83 * transport and result parser to get it.
84 */
85
87#include <algorithm>
88#include <cmath>
89#include <cstddef>
90#include <cstdio>
91#include <cstdlib>
92#include <cstring>
93#include <fstream>
94#include <limits>
95#include <sstream>
96#include <string>
97#include <utility>
98#include <vector>
99
100#include <sys/stat.h>
101#include <sys/utsname.h>
102#include <unistd.h>
103
104#include "json.hpp"
108#include "line/util/error.h"
109#include "line/util/http.h"
110#include "line/util/matrix.h"
111#include "line/util/subprocess.h"
112#include "line/util/tempdir.h"
113
114namespace line {
115namespace ldes {
116
117namespace detail {
118
119using Json = nlohmann::json;
120
121/**
122 * The shortest decimal that reads back as `v`, which is what Python's `repr`
123 * emits on the same flag. A fixed `%.10g`, as `solveCli.m` uses, would round a
124 * tolerance the caller set exactly, and the engine parses whatever it is given.
125 */
126inline std::string shortest(double v) {
127 char buf[64];
128 for (int prec = 15; prec <= 17; ++prec) {
129 std::snprintf(buf, sizeof(buf), "%.*g", prec, v);
130 if (std::strtod(buf, nullptr) == v) return std::string(buf);
131 }
132 return std::string(buf);
133}
134
135/** A JSON array of arrays as a matrix, with `null` read back as NaN. */
136inline Matrix<double> mat(const Json& v) {
137 if (v.is_null() || !v.is_array() || v.empty()) return Matrix<double>();
138 if (!v[0].is_array()) {
139 Matrix<double> m(1, v.size());
140 for (std::size_t j = 0; j < v.size(); ++j)
141 m(0, j) = v[j].is_null() ? std::numeric_limits<double>::quiet_NaN()
142 : v[j].get<double>();
143 return m;
144 }
145 std::size_t cols = 0;
146 for (std::size_t i = 0; i < v.size(); ++i)
147 if (v[i].is_array() && v[i].size() > cols) cols = v[i].size();
148 Matrix<double> m(v.size(), cols, std::numeric_limits<double>::quiet_NaN());
149 for (std::size_t i = 0; i < v.size(); ++i) {
150 if (!v[i].is_array()) continue;
151 for (std::size_t j = 0; j < v[i].size(); ++j)
152 if (!v[i][j].is_null()) m(i, j) = v[i][j].get<double>();
153 }
154 return m;
155}
156
157/** A JSON array as a vector of doubles, `null` read back as NaN. */
158inline std::vector<double> vec(const Json& v) {
159 std::vector<double> out;
160 if (!v.is_array()) return out;
161 out.reserve(v.size());
162 for (std::size_t i = 0; i < v.size(); ++i)
163 out.push_back(v[i].is_null() ? std::numeric_limits<double>::quiet_NaN()
164 : v[i].get<double>());
165 return out;
166}
167
168inline const Json& field(const Json& o, const char* key) {
169 static const Json null_value;
170 return o.is_object() && o.contains(key) ? o[key] : null_value;
171}
172
173/** Reads a whole file, or throws naming it. */
174inline std::string read_file(const std::string& path) {
175 std::ifstream in(path.c_str(), std::ios::binary);
176 if (!in) throw InputError("SolverLDES: cannot open the model document '" + path + "'");
177 std::ostringstream ss;
178 ss << in.rdbuf();
179 return ss.str();
180}
181
182inline void write_file(const std::string& path, const std::string& text) {
183 std::ofstream out(path.c_str(), std::ios::binary);
184 if (!out) throw InputError("SolverLDES: cannot write '" + path + "'");
185 out << text;
186}
187
188/**
189 * True when the DOCUMENT uses something the prebuilt AOT image predates.
190 *
191 * The two other clients walk the live model for `costCap`, `markedClasses` and
192 * `lcdScaling`; the same three are wire keys, so the scan happens here on the
193 * bytes that actually cross. A substring test over the document is deliberate:
194 * every one of these keys is unique enough that a false positive only reorders
195 * the runners, which costs startup time and changes no number, while a false
196 * negative would silently simulate a different model.
197 */
198inline bool document_postdates_native(const std::string& doc) {
199 static const char* keys[] = {"\"costCaps\"", "\"markedClasses\"", "\"classDependence\"",
200 "\"loadDependence\"", "\"jointDependence\""};
201 for (std::size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i)
202 if (doc.find(keys[i]) != std::string::npos) return true;
203 return false;
204}
205
206} // namespace detail
207
208/**
209 * The engine flags of one run, after `solve <model> -o <result>`.
210 *
211 * ONE MAPPING for the subprocess and the REST paths, as in both other clients:
212 * the server takes the same long-form flags verbatim, and deriving them twice
213 * is how the two transports start disagreeing. A knob is emitted only when it
214 * differs from the engine default, so a default run yields the minimal command
215 * line an older AOT image still parses -- except `-s` and `--seed`, which are
216 * always emitted: the CLI's seed default is -1 (random), so a silent seed makes
217 * the run irreproducible and unlike the MATLAB and Python clients.
218 */
219inline std::vector<std::string> ldes_flags(const LdesOptions& o,
220 const std::vector<std::string>& extra) {
221 std::vector<std::string> f;
222 const std::size_t budget = o.events ? o.events : o.samples;
223 f.push_back("-s");
224 f.push_back(std::to_string(budget));
225 f.push_back("--seed");
226 f.push_back(std::to_string(o.seed));
227 if (!o.method.empty() && o.method != "default") {
228 f.push_back("--method");
229 f.push_back(o.method);
230 }
231 if (o.cnvgon) {
232 f.push_back("--cnvgon");
233 if (o.cnvgtol != 0.05) {
234 f.push_back("--cnvgtol");
235 f.push_back(detail::shortest(o.cnvgtol));
236 }
237 if (o.cnvgbatch != 20) {
238 f.push_back("--cnvgbatch");
239 f.push_back(std::to_string(o.cnvgbatch));
240 }
241 if (o.cnvgchk != 0) {
242 f.push_back("--cnvgchk");
243 f.push_back(std::to_string(o.cnvgchk));
244 }
245 }
246 if (o.tranfilter != "mser5") {
247 f.push_back("--tranfilter");
248 f.push_back(o.tranfilter);
249 }
250 if (o.warmupfrac != 0.2) {
251 f.push_back("--warmupfrac");
252 f.push_back(detail::shortest(o.warmupfrac));
253 }
254 if (o.mserbatch != 5) {
255 f.push_back("--mserbatch");
256 f.push_back(std::to_string(o.mserbatch));
257 }
258 if (o.cimethod != "obm") {
259 f.push_back("--cimethod");
260 f.push_back(o.cimethod);
261 }
262 if (o.obmoverlap != 0.5) {
263 f.push_back("--obmoverlap");
264 f.push_back(detail::shortest(o.obmoverlap));
265 }
266 if (o.ciminbatch != 10) {
267 f.push_back("--ciminbatch");
268 f.push_back(std::to_string(o.ciminbatch));
269 }
270 if (o.ciminobs != 100) {
271 f.push_back("--ciminobs");
272 f.push_back(std::to_string(o.ciminobs));
273 }
274 if (o.spectral_low_freq_frac != 0.25) {
275 f.push_back("--spectrallowfreqfrac");
276 f.push_back(detail::shortest(o.spectral_low_freq_frac));
277 }
278 // --slotlength implies --slotted on the CLI side; both are emitted when the
279 // length is non-default so the command line states the intent explicitly.
280 if (o.slotted) {
281 f.push_back("--slotted");
282 if (o.slot_length != 1.0) {
283 f.push_back("--slotlength");
284 f.push_back(detail::shortest(o.slot_length));
285 }
286 }
287 if (o.replications > 1) {
288 f.push_back("--replications");
289 f.push_back(std::to_string(o.replications));
290 if (o.numthreads > 0) {
291 f.push_back("--numthreads");
292 f.push_back(std::to_string(o.numthreads));
293 }
294 }
295 if (o.has_timespan) {
296 f.push_back("--timespan");
297 f.push_back(detail::shortest(o.t0) + "," + detail::shortest(o.t1));
298 }
299 if (o.timeout > 0.0 && std::isfinite(o.timeout)) {
300 f.push_back("--maxtime");
301 f.push_back(detail::shortest(o.timeout));
302 }
303 if (o.busy_period_orders > 0) {
304 f.push_back("--busyperiod");
305 f.push_back(std::to_string(o.busy_period_orders));
306 for (std::size_t i = 0; i < o.busy_period_subnets.size(); ++i) {
307 // station indexes cross the wire zero-based, as the engine holds them
308 std::string v;
309 for (std::size_t t = 0; t < o.busy_period_subnets[i].size(); ++t)
310 v += (t ? "," : "") + std::to_string(o.busy_period_subnets[i][t]);
311 f.push_back("--busyperiod-subnet");
312 f.push_back(v);
313 }
314 }
315 if (!o.init_sol.empty()) {
316 std::string v;
317 for (std::size_t i = 0; i < o.init_sol.size(); ++i)
318 v += (i ? "," : "") + detail::shortest(o.init_sol[i]);
319 f.push_back("--initsol");
320 f.push_back(v);
321 }
322 for (std::size_t i = 0; i < extra.size(); ++i) f.push_back(extra[i]);
323 return f;
324}
325
326/** One runner: the argv prefix that runs the engine, and the name of its image. */
328 std::vector<std::string> argv; ///< up to and including "solve"
329 std::string engine; ///< "native" or "jar"
330};
331
332/**
333 * The runners to try, in order (see the file header for why there are two and
334 * when the order flips).
335 *
336 * @param doc the model document, scanned for features the AOT image predates
337 * @param flags the resolved flags, scanned for the same reason
338 * @throws UnsupportedError when no engine can run on this host
339 */
340inline std::vector<LdesRunner> ldes_runners(const std::string& doc,
341 const std::vector<std::string>& flags) {
342 const std::string& dir = ldes_engine_dir();
343 std::vector<LdesRunner> out;
344 const std::string native = detail::native_ldes_path(dir);
345 if (!native.empty()) {
346 LdesRunner r;
347 r.argv.push_back(native);
348 r.argv.push_back("solve");
349 r.engine = "native";
350 out.push_back(r);
351 }
352 const std::string java = detail::find_java();
353 const std::string jar = dir.empty() ? std::string() : dir + "/ldes.jar";
354 if (!java.empty() && detail::is_file(jar)) {
355 LdesRunner r;
356 r.argv.push_back(java);
357 r.argv.push_back("-jar");
358 r.argv.push_back(jar);
359 r.argv.push_back("solve");
360 r.engine = "jar";
361 out.push_back(r);
362 }
363 if (out.empty()) throw UnsupportedError(detail::no_backend_message(dir));
364 if (out.size() > 1) {
365 // --initsol and --busyperiod used to be here, because the retired
366 // GraalVM image predated them; the C++ engine honours both and agrees
367 // with the jar to the last digit, so only --respt-samples remains.
368 bool prefer_jar = detail::document_postdates_native(doc);
369 for (std::size_t i = 0; i < flags.size() && !prefer_jar; ++i)
370 prefer_jar = flags[i] == "--respt-samples";
371 if (prefer_jar) std::swap(out[0], out[1]);
372 }
373 return out;
374}
375
376/**
377 * Solves through an LDES REST server and returns its `result` document.
378 *
379 * The payload is the model text plus the same flag vector the subprocess would
380 * have been given, so a fixed seed gives the same numbers on both transports.
381 */
382inline detail::Json ldes_solve_rest(const std::string& base_url, const std::string& doc,
383 const std::vector<std::string>& flags, double timeout) {
384 std::string url = base_url;
385 while (!url.empty() && url[url.size() - 1] == '/') url.erase(url.size() - 1);
386 if (url.size() < 6 || url.compare(url.size() - 6, 6, "/solve") != 0) url += "/api/v1/solve";
387
388 detail::Json payload = detail::Json::object();
389 detail::Json model = detail::Json::object();
390 model["content"] = doc;
391 model["base64"] = false;
392 payload["model"] = model;
393 detail::Json fl = detail::Json::array();
394 for (std::size_t i = 0; i < flags.size(); ++i) fl.push_back(flags[i]);
395 payload["flags"] = fl;
396
397 const int millis = (timeout > 0.0 && std::isfinite(timeout))
398 ? static_cast<int>((timeout + 30.0) * 1000.0)
399 : 600000;
400 const http::Response resp = http::post_json(url, payload.dump(), millis);
401 if (resp.body.empty())
402 throw NumericError("SolverLDES: the REST server at " + url + " returned HTTP " +
403 std::to_string(resp.status) + " with no body");
404 detail::Json out = detail::Json::parse(resp.body, nullptr, false);
405 if (out.is_discarded() || !out.is_object())
406 throw NumericError("SolverLDES: the REST server at " + url +
407 " returned a non-JSON body: " + resp.body);
408 if (!out.contains("status") || out["status"].get<std::string>() != "ok") {
409 const std::string msg = out.contains("message") ? out["message"].get<std::string>()
410 : std::string("unspecified error");
411 const std::string err = out.contains("stderr") ? out["stderr"].get<std::string>()
412 : std::string();
413 throw NumericError("SolverLDES: the REST solve failed: " + msg +
414 (err.empty() ? "" : " (engine stderr: " + err + ")"));
415 }
416 return out["result"];
417}
418
419/**
420 * Parses one `ldes-result` document.
421 *
422 * `format` is CHECKED, not assumed: the runner falls through on failure, and a
423 * runner that wrote some other JSON to the output path would otherwise be read
424 * as a result full of absent metrics rather than as the failure it is.
425 */
426inline LdesResult parse_ldes_result(const detail::Json& d) {
427 if (d.is_object() && d.contains("error"))
428 throw NumericError("SolverLDES: the engine reported an error: " +
429 d["error"].get<std::string>());
430 if (!d.is_object() || !d.contains("format") || d["format"].get<std::string>() != "ldes-result")
431 throw NumericError(
432 "SolverLDES: the engine wrote a document that is not an ldes-result; the run did not "
433 "produce metrics");
434
435 LdesResult r;
436 r.method = d.contains("method") && !d["method"].is_null() ? d["method"].get<std::string>()
437 : std::string("default");
438 if (d.contains("runtime")) r.runtime = d["runtime"].get<double>();
439 if (d.contains("converged")) r.converged = d["converged"].get<bool>();
440 if (d.contains("stoppingReason") && !d["stoppingReason"].is_null())
441 r.stopping_reason = d["stoppingReason"].get<std::string>();
442 if (d.contains("convergenceBatches")) r.convergence_batches = d["convergenceBatches"].get<long>();
443 if (d.contains("totalSimulatedEvents"))
444 r.total_simulated_events = d["totalSimulatedEvents"].get<long long>();
445
446 const detail::Json& dim = detail::field(d, "dimensions");
447 if (dim.is_object()) {
448 if (dim.contains("nstations")) r.nstations = dim["nstations"].get<std::size_t>();
449 if (dim.contains("nclasses")) r.nclasses = dim["nclasses"].get<std::size_t>();
450 if (dim.contains("nchains")) r.nchains = dim["nchains"].get<std::size_t>();
451 if (dim.contains("stationNames"))
452 for (std::size_t i = 0; i < dim["stationNames"].size(); ++i)
453 r.station_names.push_back(dim["stationNames"][i].get<std::string>());
454 if (dim.contains("classNames"))
455 for (std::size_t i = 0; i < dim["classNames"].size(); ++i)
456 r.class_names.push_back(dim["classNames"][i].get<std::string>());
457 }
458
459 const detail::Json& m = detail::field(d, "metrics");
460 r.QN = detail::mat(detail::field(m, "QN"));
461 r.UN = detail::mat(detail::field(m, "UN"));
462 r.RN = detail::mat(detail::field(m, "RN"));
463 r.TN = detail::mat(detail::field(m, "TN"));
464 r.AN = detail::mat(detail::field(m, "AN"));
465 r.WN = detail::mat(detail::field(m, "WN"));
466 r.CN = detail::mat(detail::field(m, "CN"));
467 r.XN = detail::mat(detail::field(m, "XN"));
468 r.DropRateJoin = detail::mat(detail::field(m, "DropRateJoin"));
469
470 const detail::Json& ci = detail::field(d, "confidenceIntervals");
471 r.QNCI = detail::mat(detail::field(ci, "QNCI"));
472 r.UNCI = detail::mat(detail::field(ci, "UNCI"));
473 r.RNCI = detail::mat(detail::field(ci, "RNCI"));
474 r.TNCI = detail::mat(detail::field(ci, "TNCI"));
475 r.ANCI = detail::mat(detail::field(ci, "ANCI"));
476 r.WNCI = detail::mat(detail::field(ci, "WNCI"));
477
478 const detail::Json& rp = detail::field(d, "relativePrecision");
479 r.QNRelPrec = detail::mat(detail::field(rp, "QNRelPrec"));
480 r.UNRelPrec = detail::mat(detail::field(rp, "UNRelPrec"));
481 r.RNRelPrec = detail::mat(detail::field(rp, "RNRelPrec"));
482 r.TNRelPrec = detail::mat(detail::field(rp, "TNRelPrec"));
483
484 const detail::Json& sc = detail::field(d, "sampleCounts");
485 r.QNSamples = detail::mat(detail::field(sc, "QNSamples"));
486 r.UNSamples = detail::mat(detail::field(sc, "UNSamples"));
487 r.RNSamples = detail::mat(detail::field(sc, "RNSamples"));
488 r.TNSamples = detail::mat(detail::field(sc, "TNSamples"));
489
490 const detail::Json& fcr = detail::field(d, "fcr");
491 if (fcr.is_object()) {
492 if (fcr.contains("nregions")) r.nregions = fcr["nregions"].get<std::size_t>();
493 r.QNfcr = detail::mat(detail::field(fcr, "QNfcr"));
494 r.UNfcr = detail::mat(detail::field(fcr, "UNfcr"));
495 r.RNfcr = detail::mat(detail::field(fcr, "RNfcr"));
496 r.TNfcr = detail::mat(detail::field(fcr, "TNfcr"));
497 r.ANfcr = detail::mat(detail::field(fcr, "ANfcr"));
498 r.WNfcr = detail::mat(detail::field(fcr, "WNfcr"));
499 r.WeightNfcr = detail::mat(detail::field(fcr, "WeightNfcr"));
500 r.MemOccNfcr = detail::mat(detail::field(fcr, "MemOccNfcr"));
501 r.DropRateNfcr = detail::mat(detail::field(fcr, "DropRateNfcr"));
502 }
503
504 const detail::Json& imp = detail::field(d, "impatience");
505 if (imp.is_object()) {
506 r.renegedCustomers = detail::mat(detail::field(imp, "renegedCustomers"));
507 r.avgRenegingWaitTime = detail::mat(detail::field(imp, "avgRenegingWaitTime"));
508 r.renegingRate = detail::mat(detail::field(imp, "renegingRate"));
509 r.balkedCustomers = detail::mat(detail::field(imp, "balkedCustomers"));
510 r.balkingProbability = detail::mat(detail::field(imp, "balkingProbability"));
511 r.retriedCustomers = detail::mat(detail::field(imp, "retriedCustomers"));
512 r.retrialDropped = detail::mat(detail::field(imp, "retrialDropped"));
513 r.avgOrbitSize = detail::mat(detail::field(imp, "avgOrbitSize"));
514 }
515
516 const detail::Json& cm = detail::field(d, "cacheMetrics");
517 if (cm.is_object())
518 for (detail::Json::const_iterator it = cm.begin(); it != cm.end(); ++it) {
520 c.hit = detail::mat(detail::field(it.value(), "hit"));
521 c.delayed = detail::mat(detail::field(it.value(), "delayed"));
522 c.miss = detail::mat(detail::field(it.value(), "miss"));
523 c.latency = detail::mat(detail::field(it.value(), "latency"));
524 c.hitList = detail::mat(detail::field(it.value(), "hitList"));
525 c.itemProb = detail::mat(detail::field(it.value(), "itemProb"));
526 c.listCost = detail::mat(detail::field(it.value(), "listCost"));
527 r.cache_metrics[it.key()] = c;
528 }
529
530 const detail::Json& bp = detail::field(d, "busyPeriods");
531 if (bp.is_object()) {
532 const detail::Json& targets = detail::field(bp, "targets");
533 for (std::size_t i = 0; i < targets.size(); ++i) {
535 const detail::Json& tj = targets[i];
536 if (tj.contains("name") && tj["name"].is_string())
537 t.name = tj["name"].get<std::string>();
538 const detail::Json& st = detail::field(tj, "stations");
539 for (std::size_t k = 0; k < st.size(); ++k)
540 t.stations.push_back(static_cast<std::size_t>(st[k].get<double>()));
541 if (tj.contains("class") && tj["class"].is_number())
542 t.job_class = static_cast<int>(tj["class"].get<double>());
543 t.mean = detail::vec(detail::field(tj, "mean"));
544 t.count = detail::vec(detail::field(tj, "count"));
545 r.busy_periods.push_back(t);
546 }
547 }
548
549 const detail::Json& hist = detail::field(d, "stateHistogram");
550 if (hist.is_object()) {
551 r.histogram_space = detail::mat(detail::field(hist, "space"));
552 r.histogram_time = detail::mat(detail::field(hist, "time"));
553 r.traj_space = detail::mat(detail::field(hist, "trajSpace"));
554 r.traj_time = detail::mat(detail::field(hist, "trajTime"));
555 }
556
557 // `respTimeSamples` appears at TOP LEVEL under --respt-samples and inside
558 // `transient` under --trajectory; the two are the same measurement and
559 // whichever is present is read.
560 const detail::Json& tran = detail::field(d, "transient");
561 if (tran.is_object()) {
562 r.t = detail::vec(detail::field(tran, "t"));
563 const char* keys[3] = {"QNt", "UNt", "TNt"};
564 std::vector<std::vector<Matrix<double>>>* dst[3] = {&r.QNt, &r.UNt, &r.TNt};
565 for (int q = 0; q < 3; ++q) {
566 const detail::Json& blk = detail::field(tran, keys[q]);
567 if (!blk.is_array()) continue;
568 for (std::size_t i = 0; i < blk.size(); ++i) {
569 std::vector<Matrix<double>> row;
570 for (std::size_t k = 0; k < blk[i].size(); ++k)
571 row.push_back(detail::mat(blk[i][k]));
572 dst[q]->push_back(row);
573 }
574 }
575 }
576 const detail::Json& rts = tran.is_object() && tran.contains("respTimeSamples")
577 ? tran["respTimeSamples"]
578 : detail::field(d, "respTimeSamples");
579 if (rts.is_array())
580 for (std::size_t i = 0; i < rts.size(); ++i) {
581 std::vector<std::vector<double>> row;
582 for (std::size_t k = 0; k < rts[i].size(); ++k) row.push_back(detail::vec(rts[i][k]));
583 r.respTimeSamples.push_back(row);
584 }
585 return r;
586}
587
588/**
589 * Runs one LDES simulation on a `model.json` DOCUMENT and parses its result.
590 *
591 * The document is written into a private scratch directory, the engine is run
592 * there, and the directory is removed on both the success and the failure path
593 * (`TempDir`'s destructor), because a wrapper that leaks one directory per
594 * failed solve leaks silently.
595 *
596 * THE RUNNERS ARE TRIED IN ORDER and the LAST failure is reported in full,
597 * merged stdout and stderr, because the engine states why it refused there and
598 * an exit code alone is not a diagnosis.
599 *
600 * @param doc the model.json text, forwarded byte for byte
601 * @param o the run's knobs
602 * @param extra_flags flags an analysis adds (--trajectory, --export-histogram,
603 * --respt-samples)
604 * @return the parsed result, carrying which image produced it
605 */
606inline LdesResult solver_ldes_text(const std::string& doc, const LdesOptions& o,
607 const std::vector<std::string>& extra_flags =
608 std::vector<std::string>()) {
609 const std::vector<std::string> flags = ldes_flags(o, extra_flags);
610 util::TempDir tmp("ldes");
611 const std::string model_path = tmp.file("model.json");
612 const std::string result_path = tmp.file("result.json");
613 line::util::LineConsole::step("serializing the model to JSON for the LDES engine");
614 detail::write_file(model_path, doc);
615
616 if (!o.rest_url.empty()) {
618 r.engine = "rest";
619 return r;
620 }
621
622 const std::vector<LdesRunner> runners = ldes_runners(doc, flags);
623 // The cooperative --maxtime stops the event loop; this is the HARD bound and
624 // leaves the engine 30 s of grace to write its result and exit, else no
625 // result file is produced at all. An infinite budget falls back to 600 s.
626 const int hard = (o.timeout > 0.0 && std::isfinite(o.timeout))
627 ? static_cast<int>(o.timeout + 30.0)
628 : 600;
629 std::string last_err;
630 for (std::size_t i = 0; i < runners.size(); ++i) {
631 std::vector<std::string> argv = runners[i].argv;
632 argv.push_back(model_path);
633 argv.push_back("-o");
634 argv.push_back(result_path);
635 for (std::size_t j = 0; j < flags.size(); ++j) argv.push_back(flags[j]);
636 std::remove(result_path.c_str());
637 if (o.verbose) {
638 std::string line;
639 for (std::size_t j = 0; j < argv.size(); ++j) line += (j ? " " : "") + argv[j];
640 std::fprintf(stderr, "SolverLDES command: %s\n", line.c_str());
641 }
642 line::util::LineConsole::step("running the LDES engine (%s) as a subprocess",
643 runners[i].engine.c_str());
644 const util::ProcResult p = util::capture(argv, hard, true);
645 if (p.timedOut) {
646 LdesResult r;
647 r.timed_out = true;
648 r.stopping_reason = "max_time";
649 r.engine = runners[i].engine;
650 return r;
651 }
652 if (p.exitCode == 0 && detail::is_file(result_path)) {
653 line::util::LineConsole::step("parsing the LDES result document");
654 const std::string text = detail::read_file(result_path);
655 detail::Json parsed = detail::Json::parse(text, nullptr, false);
656 if (parsed.is_discarded())
657 throw NumericError("SolverLDES: the engine wrote a result that is not JSON");
658 LdesResult r = parse_ldes_result(parsed);
659 r.engine = runners[i].engine;
660 return r;
661 }
662 last_err = "exit code " + std::to_string(p.exitCode) + ": " + util::trim(p.out);
663 }
664 throw NumericError("SolverLDES: the engine failed on all " +
665 std::to_string(runners.size()) + " runner(s); " + last_err);
666}
667
668/** The same, reading the document from a file. */
669inline LdesResult solver_ldes_file(const std::string& path, const LdesOptions& o,
670 const std::vector<std::string>& extra_flags =
671 std::vector<std::string>()) {
672 return solver_ldes_text(detail::read_file(path), o, extra_flags);
673}
674
675/**
676 * One constraint of a joint-state query: a station and the per-class job counts
677 * it is required to hold.
678 *
679 * `station` is 1-BASED, matching `sn.nodeToStation`. `counts` is read up to
680 * `nclasses` entries; a shorter vector constrains only the classes it names,
681 * which is how a caller asks for "two class-1 jobs here, any number of the
682 * rest" without enumerating the remainder.
683 */
685 std::size_t station = 0;
686 std::vector<double> counts;
687};
688
689/**
690 * Residence-time probability of an aggregate joint state, from a parsed result.
691 *
692 * WHY THE HISTOGRAM AND NOT THE TRAJECTORY. `stateHistogram` is the exact
693 * residence time of every integer joint state the run visited, keyed on the
694 * state itself (`Solver_ssj.updateRewardStats` accumulates it per marking), so
695 * P(state) = t(state) / sum(t) is an unbiased estimate that is exact on the
696 * sampled path. The transient `QNt` series is a sequence of INTERVAL MEANS of
697 * the queue length; comparing one against an integer state matches only where a
698 * bucket mean happens to land on an integer, which is why the three clients
699 * that estimated a probability that way reported a near-zero number for a state
700 * the chain occupies most of the time (BUG-96).
701 *
702 * The layout is the aggregated station-major, class-minor one
703 * `ctmc_state_space_aggr` builds: column `(i-1)*nclasses + k` is the number of
704 * class-k jobs at station i. THE RESOLUTION IS PER CLASS, not per phase: the
705 * engine records the integer queue lengths, so a phase-resolved query cannot be
706 * answered from this document and the caller must aggregate first.
707 *
708 * @param r a result produced with `--export-histogram`
709 * @param nclasses the model's class count, which fixes the column stride
710 * @param query the stations to constrain, and to what
711 * @return the residence-time fraction, 0 when the state was never visited
712 * @throws NumericError when the run carried no histogram (the flag was omitted)
713 */
714inline double ldes_prob_from_histogram(const LdesResult& r, std::size_t nclasses,
715 const std::vector<LdesStateQuery>& query) {
716 if (r.histogram_space.rows() == 0 || r.histogram_time.rows() == 0)
717 throw NumericError(
718 "SolverLDES: this result carries no state histogram, so no state probability can be "
719 "read from it; the run has to pass --export-histogram");
720 if (nclasses == 0)
721 throw InputError("SolverLDES: a state probability needs the model's class count");
722 const std::size_t nstates = r.histogram_space.rows();
723 double total = 0.0;
724 for (std::size_t s = 0; s < nstates; ++s) total += r.histogram_time(s, 0);
725 if (!(total > 0.0)) return 0.0;
726
727 const std::size_t ncols = r.histogram_space.cols();
728 for (std::size_t q = 0; q < query.size(); ++q) {
729 if (query[q].station == 0)
730 throw InputError("SolverLDES: a state query names station 0; stations are 1-based");
731 const std::size_t last = (query[q].station - 1) * nclasses + nclasses;
732 if (last > ncols)
733 throw InputError(
734 "SolverLDES: station " + std::to_string(query[q].station) +
735 " is past the end of the state histogram, which holds " + std::to_string(ncols) +
736 " columns for " + std::to_string(nclasses) + " classes");
737 }
738
739 double matched = 0.0;
740 for (std::size_t s = 0; s < nstates; ++s) {
741 bool ok = true;
742 for (std::size_t q = 0; q < query.size() && ok; ++q) {
743 const std::size_t base = (query[q].station - 1) * nclasses;
744 const std::size_t n = std::min(nclasses, query[q].counts.size());
745 for (std::size_t k = 0; k < n; ++k)
746 if (std::fabs(r.histogram_space(s, base + k) - query[q].counts[k]) > 1e-9) {
747 ok = false;
748 break;
749 }
750 }
751 if (ok) matched += r.histogram_time(s, 0);
752 }
753 return matched / total;
754}
755
756/**
757 * Port of `getCdfRespT`: the EMPIRICAL response time CDF of one (station,
758 * class) pair, built from the per-job samples the engine exports under
759 * `--respt-samples`.
760 *
761 * The returned matrix is (n x 2) with columns [F(t), t], NOT [t, F(t)]. That
762 * order is the convention every `getCdfRespT` follows across the codebases
763 * (MATLAB `@SolverLDES/getCdfRespT.m`, Python `SolverJMT.getCdfRespT`), and it
764 * is the REVERSE of the transient `getTranCdfRespT`, which is a different
765 * method with a different contract. Swapping them yields a CDF that reads as a
766 * time axis and vice versa, with no error anywhere.
767 *
768 * A pair the run observed nothing at returns an EMPTY matrix rather than a
769 * fabricated law: an analytical fallback carrying the right mean says nothing
770 * about the tail, which is the whole reason to ask a simulator for a CDF.
771 * Repeated observations are collapsed, keeping the largest CDF value at each
772 * distinct time, or the ecdf is multivalued and an interpolating consumer
773 * reads a quantile off whichever duplicate it happens to hit.
774 */
775inline Matrix<double> ldes_cdf_respt(const LdesResult& r, std::size_t station,
776 std::size_t job_class) {
777 if (station >= r.respTimeSamples.size() ||
778 job_class >= r.respTimeSamples[station].size())
779 return Matrix<double>();
780 std::vector<double> x = r.respTimeSamples[station][job_class];
781 if (x.empty()) return Matrix<double>();
782 std::sort(x.begin(), x.end());
783 const std::size_t n = x.size();
784 std::vector<double> tu, fu;
785 for (std::size_t i = 0; i < n; ++i) {
786 const double f = static_cast<double>(i + 1) / static_cast<double>(n);
787 if (i + 1 < n && x[i + 1] == x[i]) continue; // keep the LAST of a tie
788 tu.push_back(x[i]);
789 fu.push_back(f);
790 }
791 Matrix<double> out(tu.size(), 2, 0.0);
792 for (std::size_t i = 0; i < tu.size(); ++i) {
793 out(i, 0) = fu[i];
794 out(i, 1) = tu[i];
795 }
796 return out;
797}
798
799/**
800 * Port of `getProbAggr`: the marginal probability of a per-class job count at
801 * one station.
802 *
803 * The run is a plain steady-state solve with `--export-histogram` added, which
804 * is what `getAvgReward` already does, so the estimate is over the same
805 * post-warmup path the mean metrics come from.
806 *
807 * @param doc the model.json document, forwarded byte for byte
808 * @param o the run's knobs
809 * @param station 1-based station index
810 * @param counts per-class job counts to test at that station
811 * @param nclasses the model's class count
812 */
813inline double ldes_prob_aggr(const std::string& doc, const LdesOptions& o, std::size_t station,
814 const std::vector<double>& counts, std::size_t nclasses) {
815 std::vector<std::string> flags;
816 flags.push_back("--export-histogram");
817 const LdesResult r = solver_ldes_text(doc, o, flags);
818 std::vector<LdesStateQuery> q(1);
819 q[0].station = station;
820 q[0].counts = counts;
821 return ldes_prob_from_histogram(r, nclasses, q);
822}
823
824/**
825 * Port of `getProbSysAggr`: the joint probability of a whole aggregate state.
826 *
827 * @param target (nstations x nclasses) job counts, EVERY station constrained
828 */
829inline double ldes_prob_sys_aggr(const std::string& doc, const LdesOptions& o,
830 const Matrix<double>& target) {
831 if (target.rows() == 0 || target.cols() == 0)
832 throw InputError("SolverLDES: getProbSysAggr needs an (nstations x nclasses) target state");
833 std::vector<std::string> flags;
834 flags.push_back("--export-histogram");
835 const LdesResult r = solver_ldes_text(doc, o, flags);
836 const std::size_t K = target.cols();
837 std::vector<LdesStateQuery> q(target.rows());
838 for (std::size_t i = 0; i < target.rows(); ++i) {
839 q[i].station = i + 1;
840 q[i].counts.resize(K);
841 for (std::size_t k = 0; k < K; ++k) q[i].counts[k] = target(i, k);
842 }
843 return ldes_prob_from_histogram(r, K, q);
844}
845
846/**
847 * The same, for a model built through the C++ API.
848 *
849 * The struct is serialized with `io::network_json_envelope`, the writer whose
850 * output the reference readers consume, and the document then takes the same
851 * path as any other. WHAT THE STRUCT CANNOT CARRY DOES NOT CROSS: a reward built
852 * from a lambda and any construct outside the writer's schema are dropped by the
853 * writer, with its own warning, which is why a caller holding a `model.json`
854 * should pass the DOCUMENT rather than a struct parsed from it -- the round trip
855 * can only lose.
856 */
857template <class T>
859 const std::vector<std::string>& extra_flags =
860 std::vector<std::string>()) {
861 return solver_ldes_text(io::network_json_envelope(sn).dump(), o, extra_flags);
862}
863
864} // namespace ldes
865} // namespace line
866
867#endif // LINE_SOLVERS_WRAPPERS_LDES_SOLVER_LDES_H
InputError(const std::string &what)
Definition error.h:39
std::size_t size() const
Definition matrix.h:91
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
static void step(const char *fmt,...)
Write one progress line.
std::string file(const std::string &name) const
A file inside it.
Definition tempdir.h:127
The exception types the port throws.
Minimal HTTP/1.1 client, enough to talk to a line-*-rest service.
The option and result records of SolverLDES, the discrete-event simulator.
Where the LDES engine is, and whether this machine can run it.
Running progress log of a LINE solver run (the "solver console").
Dense matrix and non-owning view.
Response post_json(const std::string &url, const std::string &json, int timeoutMillis)
POST a JSON document.
Definition http.h:361
Response get(const std::string &url, int timeoutMillis)
GET a URL.
Definition http.h:349
detail::json network_json_envelope(const qn::NetworkStruct< T > &sn)
The complete model.json envelope: {format, version, model}.
double ldes_prob_from_histogram(const LdesResult &r, std::size_t nclasses, const std::vector< LdesStateQuery > &query)
Residence-time probability of an aggregate joint state, from a parsed result.
LdesResult solver_ldes_text(const std::string &doc, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
Runs one LDES simulation on a model.json DOCUMENT and parses its result.
double ldes_prob_sys_aggr(const std::string &doc, const LdesOptions &o, const Matrix< double > &target)
Port of getProbSysAggr: the joint probability of a whole aggregate state.
LdesResult solver_ldes_file(const std::string &path, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
The same, reading the document from a file.
const std::string & ldes_engine_dir()
The directory holding the engine, or empty when there is none.
Definition ldes_probe.h:239
std::vector< std::string > ldes_flags(const LdesOptions &o, const std::vector< std::string > &extra)
The engine flags of one run, after solve <model> -o <result>.
Matrix< double > ldes_cdf_respt(const LdesResult &r, std::size_t station, std::size_t job_class)
Port of getCdfRespT: the EMPIRICAL response time CDF of one (station, class) pair,...
LdesResult parse_ldes_result(const detail::Json &d)
Parses one ldes-result document.
double ldes_prob_aggr(const std::string &doc, const LdesOptions &o, std::size_t station, const std::vector< double > &counts, std::size_t nclasses)
Port of getProbAggr: the marginal probability of a per-class job count at one station.
detail::Json ldes_solve_rest(const std::string &base_url, const std::string &doc, const std::vector< std::string > &flags, double timeout)
Solves through an LDES REST server and returns its result document.
LdesResult solver_ldes(const qn::NetworkStruct< T > &sn, const LdesOptions &o, const std::vector< std::string > &extra_flags=std::vector< std::string >())
The same, for a model built through the C++ API.
std::vector< LdesRunner > ldes_runners(const std::string &doc, const std::vector< std::string > &flags)
The runners to try, in order (see the file header for why there are two and when the order flips).
ProcResult capture(const std::vector< std::string > &argv, int timeoutSeconds, bool mergeStderr=false)
Runs a command, capturing stdout and discarding stderr.
Definition subprocess.h:82
std::string trim(const std::string &s)
Trims ASCII whitespace from both ends, as Java's String.trim() does.
Definition subprocess.h:181
qn::NetworkStruct -> model.json, the inverse of network_reader.h.
An HTTP response, with the body already de-chunked.
Definition http.h:60
std::string body
Response body, decoded.
Definition http.h:62
int status
HTTP status code.
Definition http.h:61
Per-cache hit/miss/latency, as the cacheMetrics block carries them.
Matrix< double > listCost
(1 x nlists) mean storage cost held per list
Matrix< double > latency
(1 x nclasses) expected retrieval latency
Matrix< double > hitList
(nclasses x nlists) hit probability by list
Matrix< double > itemProb
(nitems x nlists+1) item position law
Matrix< double > delayed
(1 x nclasses) delayed-hit probability
Matrix< double > miss
(1 x nclasses) miss probability
Matrix< double > hit
(1 x nclasses) hit probability
The knobs of one LDES run.
double warmupfrac
–warmupfrac, only for tranfilter=fixed
int ciminbatch
–ciminbatch
double spectral_low_freq_frac
–spectrallowfreqfrac
bool verbose
echo the resolved command line before running it
double cnvgtol
–cnvgtol
std::string rest_url
Base URL of an LDES REST server (the imperialqore/ldes container).
double obmoverlap
–obmoverlap; 0 reduces OBM to plain batch means
std::vector< std::vector< std::size_t > > busy_period_subnets
–busyperiod-subnet, zero-based station indexes, one flag per set.
long seed
–seed; -1 requests a random stream
bool slotted
–slotted, run on the slot lattice
int mserbatch
–mserbatch, MSER batch size
std::string cimethod
–cimethod: obm | bm | spectral | none
double slot_length
–slotlength
int cnvgchk
–cnvgchk, events between checks; 0 = samples/50
int replications
–replications; 0 = not given (one path)
int ciminobs
–ciminobs, below which no CI is reported
std::size_t events
0 = not given; overrides samples when set
int cnvgbatch
–cnvgbatch, batches before the first check
std::string tranfilter
–tranfilter: mser5 | fixed | none
std::vector< double > init_sol
–initsol, the warm-start placement as a STATION-MAJOR vector [st0_cl0, st0_cl1, .....
int numthreads
–numthreads; 0 = not given
std::size_t samples
-s, service-completion budget
double timeout
–maxtime, a COOPERATIVE wall-clock budget the event loop polls.
bool has_timespan
true when [t0,t1] was set: a TRANSIENT run
int busy_period_orders
–busyperiod: highest busy period order measured, 0 disabling the measurement.
One measured busy period target (–busyperiod): a station, a station-class pair, or a declared station...
std::vector< std::size_t > stations
station indexes, zero-based
One ldes-result document, parsed.
Matrix< double > balkedCustomers
Matrix< double > QNSamples
std::map< std::string, LdesCacheMetrics > cache_metrics
Per-cache metrics, keyed by the Cache NODE name.
Matrix< double > avgOrbitSize
Matrix< double > QNRelPrec
Matrix< double > TNCI
Matrix< double > WNfcr
std::vector< std::vector< Matrix< double > > > QNt
[STATION][class] -> (npoints x 2), columns [value, time].
Matrix< double > renegingRate
Matrix< double > XN
(1 x nclasses), per-class visits and system tput
std::vector< std::vector< std::vector< double > > > respTimeSamples
[station][class] -> the per-job response times the engine recorded.
Matrix< double > UNfcr
Matrix< double > WeightNfcr
Matrix< double > RNRelPrec
Matrix< double > avgRenegingWaitTime
Matrix< double > QNfcr
Matrix< double > traj_space
Matrix< double > UN
Matrix< double > DropRateNfcr
Matrix< double > histogram_space
The exact joint-state residence-time histogram (–export-histogram).
std::string stopping_reason
convergence | max_events | max_sim_events | max_time.
std::vector< std::vector< Matrix< double > > > UNt
std::vector< std::string > class_names
Matrix< double > TNRelPrec
Matrix< double > TN
Matrix< double > UNCI
Matrix< double > TNSamples
Matrix< double > RN
Matrix< double > UNRelPrec
std::string engine
"native" or "jar": which runner produced these numbers.
Matrix< double > QNCI
Matrix< double > traj_time
std::vector< BusyPeriodTarget > busy_periods
Matrix< double > CN
Matrix< double > RNfcr
Matrix< double > WN
Matrix< double > retriedCustomers
Matrix< double > retrialDropped
Matrix< double > AN
long long total_simulated_events
Matrix< double > MemOccNfcr
Matrix< double > TNfcr
std::vector< std::string > station_names
Matrix< double > DropRateJoin
quorum-Join sibling drops, Join rows only
Matrix< double > UNSamples
bool timed_out
True when the HARD subprocess bound fired, not the cooperative one.
Matrix< double > balkingProbability
Matrix< double > renegedCustomers
Matrix< double > histogram_time
Matrix< double > RNSamples
Matrix< double > RNCI
std::vector< double > t
the time vector, empty on a steady-state run
Matrix< double > WNCI
Matrix< double > QN
std::vector< std::vector< Matrix< double > > > TNt
Matrix< double > ANfcr
Matrix< double > ANCI
One runner: the argv prefix that runs the engine, and the name of its image.
std::vector< std::string > argv
up to and including "solve"
std::string engine
"native" or "jar"
One constraint of a joint-state query: a station and the per-class job counts it is required to hold.
std::vector< double > counts
Outcome of a captured command.
Definition subprocess.h:42
int exitCode
Exit status, or -1 when the command could not run.
Definition subprocess.h:43
bool timedOut
True when the deadline expired and the child was killed.
Definition subprocess.h:45
std::string out
Everything the command wrote to stdout.
Definition subprocess.h:44
Running an external command and capturing its output, with a deadline.
A scratch directory for the subprocess wrappers, the port's lineTempName.