#include "app/report.h" #include namespace sots::app { namespace { std::string JsonEscape(const std::string& s) { std::string o; for (char c : s) { switch (c) { case '"': o += "\\\""; break; case '\\': o += "\\\\"; break; case '\n': o += "\\n"; break; case '\r': o += "\\r"; break; case '\t': o += "\\t"; break; default: if (static_cast(c) < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; } else { o += c; } } } return o; } void TallyJson(std::ofstream& f, const char* key, const PhaseTally& t) { f << " \"" << key << "\": {\"total\": " << t.total << ", \"verified\": " << t.verified << ", \"implemented\": " << t.implemented << ", \"partial\": " << t.partial << ", \"blocked\": " << t.blocked << ", \"stub\": " << t.stub << ", \"modelled\": " << t.modelled() << ", \"committed\": " << t.committed() << "}"; } } // namespace void PrintPhaseLog(std::FILE* out, const TurnResult& r, bool verbose) { Driver current = static_cast(-1); for (const auto& rec : r.records) { if (rec.desc->driver != current) { current = rec.desc->driver; std::fprintf(out, "\n%s\n", DriverName(current)); } const char* indent = current == Driver::Player ? " " : " "; std::fprintf(out, "%s%c %-4s %-38s %-12s", indent, StatusGlyph(rec.desc->status), rec.desc->id, rec.desc->name, StatusName(rec.desc->status)); if (rec.invocations || rec.leafWrites || rec.wouldWrite || rec.rngWords) { std::fprintf(out, " ran=%-5d writes=%-5d", rec.invocations, rec.leafWrites); if (rec.wouldWrite) std::fprintf(out, " would=%-4d", rec.wouldWrite); if (rec.rngWords) std::fprintf(out, " rng=%d", rec.rngWords); } std::fprintf(out, "\n"); for (const auto& n : rec.notes) std::fprintf(out, "%s - %s\n", indent, n.c_str()); if (verbose && rec.desc->note[0]) std::fprintf(out, "%s # %s\n", indent, rec.desc->note); } } void PrintSummary(std::FILE* out, const TurnResult& r) { const PhaseTally s = TallySpine(); const PhaseTally t = TallyTail(); std::fprintf(out, "\nphases\n"); std::fprintf(out, " turn drivers (the milestone's denominator): %d of %d modelled, %d committed\n" " verified %d implemented %d partial %d blocked %d stub %d\n", s.modelled(), s.total, s.committed(), s.verified, s.implemented, s.partial, s.blocked, s.stub); std::fprintf(out, " post-combat tail (written to the autosave, tracked separately): %d of %d " "modelled\n" " verified %d implemented %d partial %d blocked %d stub %d\n", t.modelled(), t.total, t.verified, t.implemented, t.partial, t.blocked, t.stub); std::fprintf(out, "\nthis run\n"); std::fprintf(out, " leaves written %d\n", r.leafWrites); std::fprintf(out, " leaves NOT written by a blocked phase %d\n", r.wouldWrite); std::fprintf(out, " generator words consumed %d (state %s, %s)\n", r.rngWords, r.rngLoaded ? "loaded" : "UNREADABLE", r.rngCommitted ? "WRITTEN BACK" : "left untouched"); if (!r.rngUnaccounted.empty()) { std::fprintf(out, " generator words NOT accounted (never netted off the above):\n"); for (const auto& u : r.rngUnaccounted) std::fprintf(out, " - %s\n", u.c_str()); } for (const auto& w : r.warnings) std::fprintf(out, " ! %s\n", w.c_str()); } void PrintCommandReplay(std::FILE* out, const TurnResult& r) { const ReplayReport& c = r.commandReplay; if (!c.ran) { std::fprintf(out, "\nturn commands: none supplied (--turn-commands FILE). The modification " "counter can only reach the two driver bumps without a stream; the rest of\n" " its per-turn delta is the commands, and a save does not " "carry them.\n"); return; } std::fprintf(out, "\nturn commands: %d block(s), %d submitting, %d command(s)\n", c.blocks, c.submittingBlocks, c.commands); std::fprintf(out, " ModCount bumps charged %d%s\n", c.bumps, c.bumpsExact ? "" : " (LOWER BOUND -- see the warning below)"); std::fprintf(out, " applied %d transcribed %d declined %d incomplete %d\n", c.applied, c.transcribed, c.declined, c.incomplete); std::fprintf(out, " leaves written by commands %d\n", c.leafWrites); std::fprintf(out, "\n step blk player cost disposition command\n"); for (const auto& e : c.log) { const char* d = e.disposition == ReplayDisposition::Applied ? "applied" : e.disposition == ReplayDisposition::Transcribed ? "transcribed" : e.disposition == ReplayDisposition::Incomplete ? "incomplete" : "declined"; std::fprintf(out, " %4d %3d %6d %4d %-12s %s\n", e.step, e.blockIndex, e.playerId, e.bumps, d, e.what.c_str()); if (!e.reason.empty()) std::fprintf(out, " %s\n", e.reason.c_str()); } for (const auto& w : c.warnings) std::fprintf(out, " ! %s\n", w.c_str()); } bool WriteMetricJson(const std::string& path, const TurnResult& r, const std::string& inputName, const std::string& outputName) { std::ofstream f(path); if (!f) return false; const PhaseTally s = TallySpine(); const PhaseTally t = TallyTail(); f << "{\n"; f << " \"schema\": \"sots-standalone-metric/1\",\n"; f << " \"input\": \"" << JsonEscape(inputName) << "\",\n"; f << " \"output\": \"" << JsonEscape(outputName) << "\",\n"; TallyJson(f, "spine", s); f << ",\n"; TallyJson(f, "tail", t); f << ",\n"; f << " \"run\": {\"leafWrites\": " << r.leafWrites << ", \"blockedLeafWrites\": " << r.wouldWrite << ", \"rngWords\": " << r.rngWords << ", \"rngLoaded\": " << (r.rngLoaded ? "true" : "false") << ", \"rngCommitted\": " << (r.rngCommitted ? "true" : "false") << ", \"rngUnaccounted\": ["; for (std::size_t i = 0; i < r.rngUnaccounted.size(); ++i) f << (i ? ", " : "") << '"' << JsonEscape(r.rngUnaccounted[i]) << '"'; f << "]},\n"; // The command stream's own numbers, kept apart from the run's: a bump charged for a command // we declined to apply is a real bump and a fake model, and netting them would hide that. const ReplayReport& c = r.commandReplay; f << " \"turnCommands\": {\"supplied\": " << (c.ran ? "true" : "false") << ", \"blocks\": " << c.blocks << ", \"submittingBlocks\": " << c.submittingBlocks << ", \"commands\": " << c.commands << ", \"modCountBumps\": " << c.bumps << ", \"modCountExact\": " << (c.bumpsExact ? "true" : "false") << ", \"applied\": " << c.applied << ", \"transcribed\": " << c.transcribed << ", \"declined\": " << c.declined << ", \"incomplete\": " << c.incomplete << ", \"leafWrites\": " << c.leafWrites << "},\n"; f << " \"phases\": [\n"; bool first = true; for (const auto& rec : r.records) { if (!first) f << ",\n"; first = false; f << " {\"driver\": \"" << DriverName(rec.desc->driver) << "\", \"id\": \"" << rec.desc->id << "\", \"name\": \"" << rec.desc->name << "\", \"status\": \"" << StatusName(rec.desc->status) << "\", \"ran\": " << rec.invocations << ", \"writes\": " << rec.leafWrites << ", \"blockedWrites\": " << rec.wouldWrite << ", \"rng\": " << rec.rngWords << "}"; } f << "\n ]\n}\n"; return static_cast(f); } } // namespace sots::app