#include "wrap-sstream.hpp" #include "printbuffer.hpp" #include #include #include struct PrintBufferCore : public eng::opnew { // The most recent lines printed. eng::deque lines_; // Line number of the first line in the buffer. From this, all other // line numbers can be inferred. int first_line_; // Line number of the first unchecked line in the buffer. All line // numbers including this one and beyond are unchecked. // "Unchecked" lines are lines in a non-authoritative model // that haven't been verified through difference transmission. int first_unchecked_; // Constructor. PrintBufferCore() : first_line_(0), first_unchecked_(0) {} }; static PrintBufferCore shared_core; PrintBuffer::PrintBuffer() { core_ = &shared_core; } PrintBuffer::~PrintBuffer() { if (core_ != &shared_core) delete core_; } int PrintBuffer::first_line() const { return core_->first_line_; } int PrintBuffer::last_line() const { return core_->first_line_ + core_->lines_.size(); } int PrintBuffer::first_unchecked() const { return core_->first_unchecked_; } bool PrintBuffer::never_printed() const { return (core_->first_line_==0) && (core_->first_unchecked_==0) && (core_->lines_.size()==0); } const eng::string &PrintBuffer::nth(int n) const { return core_->lines_[n - core_->first_line_]; } eng::string PrintBuffer::debug_string() const { eng::ostringstream oss; oss << core_->first_line_ << "," << core_->first_unchecked_ << ":"; for (int i = 0; i < int(core_->lines_.size()); i++) { oss << core_->lines_[i] << ";"; } return oss.str(); } void PrintBuffer::clear() { if (core_ != &shared_core) { delete core_; core_ = &shared_core; } } static int first_line_len(const char *text, int len) { for (int i = 0; i < len; i++) { if (text[i] == '\n') return i; } return len; } static void add_line(PrintBufferCore *core, const char *text, int len, bool auth) { assert(core != &shared_core); core->lines_.emplace_back(text, len); if (auth) { core->first_unchecked_ = core->first_line_ + int(core->lines_.size()); } } void PrintBuffer::add_string(const eng::string &s, bool auth) { const char *text = s.c_str(); int len = s.size(); if (core_ == &shared_core) core_ = new PrintBufferCore; while (true) { int fll = first_line_len(text, len); if (fll == len) { if (len > 0) { add_line(core_, text, len, auth); } return; } else { add_line(core_, text, fll, auth); text += (fll + 1); len -= (fll + 1); } } } void PrintBuffer::discard_upto(int n) { if (core_ == &shared_core) return; while ((!core_->lines_.empty()) && (core_->first_line_ < n)) { core_->lines_.pop_front(); core_->first_line_ += 1; } if (core_->first_unchecked_ < core_->first_line_) { core_->first_unchecked_ = core_->first_line_; } } void PrintBuffer::serialize(StreamBuffer *sb) const { if (never_printed()) { sb->write_bool(true); } else { sb->write_bool(false); sb->write_uint32(core_->first_line_); sb->write_uint32(core_->first_unchecked_); sb->write_uint32(core_->lines_.size()); for (const eng::string &s : core_->lines_) { sb->write_string(s); } } } void PrintBuffer::deserialize(StreamBuffer *sb) { bool never_printed = sb->read_bool(); if (never_printed) { clear(); } else { if (core_ == &shared_core) core_ = new PrintBufferCore; core_->first_line_ = sb->read_uint32(); core_->first_unchecked_ = sb->read_uint32(); int nlines = sb->read_uint32(); core_->lines_.clear(); for (int i = 0; i < nlines; i++) { core_->lines_.push_back(sb->read_string()); } } } void PrintBuffer::diff(const PrintBuffer &auth, StreamBuffer *sb) const { // Currently, the implementation is simple. The synchronous model discards // all prediction lines from its buffer, then the server retransmits all // those lines. So this barely counts as difference transmission - it's // just transmission, regardless of what was in the synchronous model. I // think that's okay for the text console. // Ask the client to discard anything that precedes auth_first. sb->write_int32(auth.first_line()); sb->write_int32(auth.last_line()); for (int i = core_->first_unchecked_; i < auth.last_line(); i++) { eng::string line; if (i >= auth.first_line()) line = auth.nth(i); sb->write_string(line); } } void PrintBuffer::patch(StreamBuffer *sb, DebugCollector *dbc) { DebugBlock dbb(dbc, "PrintBuffer::patch"); if (core_ == &shared_core) core_ = new PrintBufferCore; int auth_first = sb->read_int32(); int auth_last = sb->read_int32(); core_->lines_.resize(core_->first_unchecked_ - core_->first_line_); int nlines = auth_last - core_->first_unchecked_; if (nlines > 0) DebugLine(dbc) << "PrintBuffer received " << nlines << " lines."; for (int i = core_->first_unchecked_; i < auth_last; i++) { core_->lines_.emplace_back(sb->read_string()); } core_->first_unchecked_ = core_->first_line_ + core_->lines_.size(); discard_upto(auth_first); if (core_->first_line_ < auth_first) { core_->first_line_ = auth_first; } } bool PrintChanneler::channel(const PrintBuffer *printbuffer, std::ostream &ostream) { if (printbuffer == nullptr) return false; if (printbuffer->first_line() > line_) { line_ = printbuffer->first_line(); } while (line_ < printbuffer->first_unchecked()) { ostream << "|" << printbuffer->nth(line_) << std::endl; line_ += 1; } return line_ > printbuffer->first_line(); } Invocation PrintChanneler::invocation(int64_t actor_id) { char buf[80]; sprintf(buf, "%" PRId64, line_); return Invocation(AccessKind::INVOKE_FLUSH_PRINTS, actor_id, actor_id, buf); } LuaDefine(unittests_printbuffer, "", "some unit tests") { PrintBuffer pbm; PrintBuffer pbs; StreamBuffer sb; // Test authoritative add_string and discard_upto. LuaAssertStrEq(L, pbm.debug_string(), "0,0:"); pbm.add_string("foo\nbar\nbaz\n", true); LuaAssertStrEq(L, pbm.debug_string(), "0,3:foo;bar;baz;"); pbm.add_string("a\nb\nc", true); LuaAssertStrEq(L, pbm.debug_string(), "0,6:foo;bar;baz;a;b;c;"); pbm.discard_upto(2); LuaAssertStrEq(L, pbm.debug_string(), "2,6:baz;a;b;c;"); pbm.discard_upto(8); LuaAssertStrEq(L, pbm.debug_string(), "6,6:"); // Test nonauthoritative add_string and discard_upto. LuaAssertStrEq(L, pbs.debug_string(), "0,0:"); pbs.add_string("foo\nbar\nbaz\n", false); LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;"); pbs.add_string("a\nb\nc", false); LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;a;b;c;"); pbs.discard_upto(2); LuaAssertStrEq(L, pbs.debug_string(), "2,2:baz;a;b;c;"); pbs.discard_upto(8); LuaAssertStrEq(L, pbs.debug_string(), "6,6:"); // Test serialization and deserialization of a nonempty printbuffer. pbm.clear(); sb.clear(); pbm.add_string("boy\nhowdy\nyeah\n", true); pbm.add_string("baby\nget\ndown\n", false); LuaAssertStrEq(L, pbm.debug_string(), "0,3:boy;howdy;yeah;baby;get;down;"); pbm.serialize(&sb); pbs.deserialize(&sb); LuaAssertStrEq(L, pbs.debug_string(), "0,3:boy;howdy;yeah;baby;get;down;"); // Test serialization and deserialization of an empty printbuffer. pbm.clear(); sb.clear(); LuaAssert(L, pbm.never_printed()); pbm.serialize(&sb); pbs.deserialize(&sb); LuaAssert(L, pbs.never_printed()); pbm.clear(); pbs.clear(); sb.clear(); pbm.add_string("foo\nbar\n", true); pbs.diff(pbm, &sb); pbs.patch(&sb, nullptr); LuaAssertStrEq(L, pbs.debug_string(), "0,2:foo;bar;"); pbm.clear(); pbm.add_string("foo\nyow\nding\ndong\n", true); pbs.diff(pbm, &sb); pbs.patch(&sb, nullptr); LuaAssertStrEq(L, pbs.debug_string(), "0,4:foo;bar;ding;dong;"); pbs.discard_upto(2); LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;"); pbs.diff(pbm, &sb); pbs.patch(&sb, nullptr); LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;"); pbs.add_string("boy\nhowdy\n", false); LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;"); pbs.diff(pbm, &sb); pbs.patch(&sb, nullptr); LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;"); pbs.add_string("boy\nhowdy\nyeah\nbaby\nget\ndown\n", false); LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;yeah;baby;get;down;"); pbs.discard_upto(5); LuaAssertStrEq(L, pbs.debug_string(), "5,5:howdy;yeah;baby;get;down;"); pbs.diff(pbm, &sb); pbs.patch(&sb, nullptr); LuaAssertStrEq(L, pbs.debug_string(), "5,5:"); return 0; }