Merge "Added flatbuffer messages for intake and 4-bar."
diff --git a/WORKSPACE b/WORKSPACE
index ab7a70f..4bb6551 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -196,12 +196,14 @@
path = "third_party/gflags",
)
+# Downloaded from:
+# https://pypi.python.org/packages/source/g/glog/glog-0.1.tar.gz
http_archive(
name = "python_glog_repo",
build_file = "@//debian:glog.BUILD",
sha256 = "953fd80122c48023d1148e6d1bda2763fcab59c8a81682bb298238a5935547b0",
strip_prefix = "glog-0.1",
- url = "https://pypi.python.org/packages/source/g/glog/glog-0.1.tar.gz",
+ url = "http://frc971.org/Build-Dependencies/glog-0.1.tar.gz",
)
bind(
@@ -651,3 +653,13 @@
strip_prefix = "halide/",
url = "http://www.frc971.org/Build-Dependencies/halide-arm32-linux-32-trunk-65c26cba6a3eca2d08a0bccf113ca28746012cc3.tgz",
)
+
+# Downloaded from:
+# https://files.pythonhosted.org/packages/05/23/7f9a896da9e7ce4170377a7a14bb804b460761f9dd66734e6ad8f001a76c/opencv_contrib_python_nonfree-4.1.1.1-cp35-cp35m-manylinux1_x86_64.whl
+http_archive(
+ name = "opencv_contrib_nonfree_amd64",
+ build_file = "@//debian:opencv_python.BUILD",
+ sha256 = "c10e7712ee1f19bf382c64fc29b5d24fa0b5bfd901eab69cef83604713e6a89e",
+ type = "zip",
+ url = "http://www.frc971.org/Build-Dependencies/opencv_contrib_python_nonfree-4.1.1.1-cp35-cp35m-manylinux1_x86_64.whl",
+)
diff --git a/aos/BUILD b/aos/BUILD
index 80ba32a..2ae0fec 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -462,6 +462,7 @@
"@com_github_google_flatbuffers//:flatbuffers",
"@com_github_google_glog//:glog",
"@com_google_absl//absl/strings",
+ "@com_google_absl//absl/types:span",
],
)
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index 80cc025..4294226 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -438,7 +438,8 @@
T::GetFullyQualifiedName(), name(), node());
CHECK(channel != nullptr)
<< ": Channel { \"name\": \"" << channel_name << "\", \"type\": \""
- << T::GetFullyQualifiedName() << "\" } not found in config.";
+ << T::GetFullyQualifiedName() << "\" } not found in config for "
+ << name() << ".";
if (!configuration::ChannelIsSendableOnNode(channel, node())) {
LOG(FATAL) << "Channel { \"name\": \"" << channel_name
diff --git a/aos/events/logging/BUILD b/aos/events/logging/BUILD
index efbbcc6..2b3df8f 100644
--- a/aos/events/logging/BUILD
+++ b/aos/events/logging/BUILD
@@ -53,6 +53,22 @@
)
cc_binary(
+ name = "log_edit",
+ srcs = [
+ "log_edit.cc",
+ ],
+ deps = [
+ ":logger",
+ "//aos:configuration",
+ "//aos:init",
+ "//aos:json_to_flatbuffer",
+ "//aos/util:file",
+ "@com_github_gflags_gflags//:gflags",
+ "@com_github_google_glog//:glog",
+ ],
+)
+
+cc_binary(
name = "log_stats",
srcs = [
"log_stats.cc",
diff --git a/aos/events/logging/log_cat.cc b/aos/events/logging/log_cat.cc
index f2427b8..7fb2378 100644
--- a/aos/events/logging/log_cat.cc
+++ b/aos/events/logging/log_cat.cc
@@ -7,16 +7,20 @@
#include "aos/json_to_flatbuffer.h"
#include "gflags/gflags.h"
-DEFINE_string(logfile, "/tmp/logfile.bfbs",
- "Name of the logfile to read from.");
DEFINE_string(
name, "",
"Name to match for printing out channels. Empty means no name filter.");
DEFINE_string(type, "",
"Channel type to match for printing out channels. Empty means no "
"type filter.");
+DEFINE_bool(raw, false,
+ "If true, just print the data out unsorted and unparsed");
+
int main(int argc, char **argv) {
gflags::SetUsageMessage(
+ "Usage:\n"
+ " log_cat [args] logfile1 logfile2 ...\n"
+ "\n"
"This program provides a basic interface to dump data from a logfile to "
"stdout. Given a logfile, channel name filter, and type filter, it will "
"print all the messages in the logfile matching the filters. The message "
@@ -27,64 +31,103 @@
"the logged data.");
aos::InitGoogle(&argc, &argv);
- aos::logger::LogReader reader(FLAGS_logfile);
- reader.Register();
-
- std::unique_ptr<aos::EventLoop> printer_event_loop =
- reader.event_loop_factory()->MakeEventLoop("printer", reader.node());
- printer_event_loop->SkipTimingReport();
- printer_event_loop->SkipAosLog();
-
- bool found_channel = false;
- const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
- reader.configuration()->channels();
- for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
- const aos::Channel *channel = channels->Get(i);
- const flatbuffers::string_view name = channel->name()->string_view();
- const flatbuffers::string_view type = channel->type()->string_view();
- if (name.find(FLAGS_name) != std::string::npos &&
- type.find(FLAGS_type) != std::string::npos) {
- if (!aos::configuration::ChannelIsReadableOnNode(
- channel, printer_event_loop->node())) {
- continue;
- }
- LOG(INFO) << "Listening on " << name << " " << type;
-
- CHECK_NOTNULL(channel->schema());
- printer_event_loop->MakeRawWatcher(
- channel, [channel](const aos::Context &context, const void *message) {
- // Print the flatbuffer out to stdout, both to remove the
- // unnecessary cruft from glog and to allow the user to readily
- // redirect just the logged output independent of any debugging
- // information on stderr.
- if (context.monotonic_remote_time != context.monotonic_event_time) {
- std::cout << context.realtime_remote_time << " ("
- << context.monotonic_remote_time << ") delivered "
- << context.realtime_event_time << " ("
- << context.monotonic_event_time << ") "
- << channel->name()->c_str() << ' '
- << channel->type()->c_str() << ": "
- << aos::FlatbufferToJson(
- channel->schema(),
- static_cast<const uint8_t *>(message))
- << std::endl;
- } else {
- std::cout << context.realtime_event_time << " ("
- << context.monotonic_event_time << ") "
- << channel->name()->c_str() << ' '
- << channel->type()->c_str() << ": "
- << aos::FlatbufferToJson(
- channel->schema(),
- static_cast<const uint8_t *>(message))
- << std::endl;
- }
- });
- found_channel = true;
+ if (FLAGS_raw) {
+ if (argc != 2) {
+ LOG(FATAL) << "Expected 1 logfile as an argument.";
}
+ aos::logger::MessageReader reader(argv[1]);
+
+ while (true) {
+ std::optional<aos::FlatbufferVector<aos::logger::MessageHeader>> message =
+ reader.ReadMessage();
+ if (!message) {
+ break;
+ }
+
+ std::cout << aos::FlatbufferToJson(message.value()) << std::endl;
+ }
+ return 0;
}
- if (!found_channel) {
- LOG(FATAL) << "Could not find any channels";
+ if (argc < 2) {
+ LOG(FATAL) << "Expected at least 1 logfile as an argument.";
+ }
+
+ std::vector<std::vector<std::string>> logfiles;
+
+ for (int i = 1; i < argc; ++i) {
+ logfiles.emplace_back(std::vector<std::string>{std::string(argv[i])});
+ }
+
+ aos::logger::LogReader reader(logfiles);
+ reader.Register();
+
+ std::vector<std::unique_ptr<aos::EventLoop>> printer_event_loops;
+
+ for (const aos::Node *node : reader.Nodes()) {
+ std::unique_ptr<aos::EventLoop> printer_event_loop =
+ reader.event_loop_factory()->MakeEventLoop("printer", node);
+ printer_event_loop->SkipTimingReport();
+ printer_event_loop->SkipAosLog();
+
+ bool found_channel = false;
+ const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
+ reader.configuration()->channels();
+ for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
+ const aos::Channel *channel = channels->Get(i);
+ const flatbuffers::string_view name = channel->name()->string_view();
+ const flatbuffers::string_view type = channel->type()->string_view();
+ if (name.find(FLAGS_name) != std::string::npos &&
+ type.find(FLAGS_type) != std::string::npos) {
+ if (!aos::configuration::ChannelIsReadableOnNode(
+ channel, printer_event_loop->node())) {
+ continue;
+ }
+ VLOG(1) << "Listening on " << name << " " << type;
+
+ std::string node_name =
+ node == nullptr ? ""
+ : std::string(node->name()->string_view()) + " ";
+
+ CHECK_NOTNULL(channel->schema());
+ printer_event_loop->MakeRawWatcher(
+ channel, [channel, node_name](const aos::Context &context,
+ const void *message) {
+ // Print the flatbuffer out to stdout, both to remove the
+ // unnecessary cruft from glog and to allow the user to readily
+ // redirect just the logged output independent of any debugging
+ // information on stderr.
+ if (context.monotonic_remote_time !=
+ context.monotonic_event_time) {
+ std::cout << node_name << context.realtime_event_time << " ("
+ << context.monotonic_event_time << ") sent "
+ << context.realtime_remote_time << " ("
+ << context.monotonic_remote_time << ") "
+ << channel->name()->c_str() << ' '
+ << channel->type()->c_str() << ": "
+ << aos::FlatbufferToJson(
+ channel->schema(),
+ static_cast<const uint8_t *>(message))
+ << std::endl;
+ } else {
+ std::cout << node_name << context.realtime_event_time << " ("
+ << context.monotonic_event_time << ") "
+ << channel->name()->c_str() << ' '
+ << channel->type()->c_str() << ": "
+ << aos::FlatbufferToJson(
+ channel->schema(),
+ static_cast<const uint8_t *>(message))
+ << std::endl;
+ }
+ });
+ found_channel = true;
+ }
+ }
+
+ if (!found_channel) {
+ LOG(FATAL) << "Could not find any channels";
+ }
+ printer_event_loops.emplace_back(std::move(printer_event_loop));
}
reader.event_loop_factory()->Run();
diff --git a/aos/events/logging/log_edit.cc b/aos/events/logging/log_edit.cc
new file mode 100644
index 0000000..5db503a
--- /dev/null
+++ b/aos/events/logging/log_edit.cc
@@ -0,0 +1,62 @@
+#include <iostream>
+
+#include "aos/configuration.h"
+#include "aos/events/logging/logger.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/util/file.h"
+#include "gflags/gflags.h"
+
+DEFINE_string(logfile, "/tmp/logfile.bfbs",
+ "Name of the logfile to read from.");
+DEFINE_bool(
+ replace, false,
+ "If true, replace the header on the log file with the JSON header.");
+DEFINE_string(
+ header, "",
+ "If provided, this is the path to the JSON with the log file header.");
+
+int main(int argc, char **argv) {
+ gflags::SetUsageMessage(
+ R"(This tool lets us manipulate log files.)");
+ aos::InitGoogle(&argc, &argv);
+
+ if (!FLAGS_header.empty()) {
+ if (FLAGS_replace) {
+ const ::std::string header_json =
+ aos::util::ReadFileToStringOrDie(FLAGS_header);
+ flatbuffers::FlatBufferBuilder fbb;
+ fbb.ForceDefaults(1);
+ flatbuffers::Offset<aos::logger::LogFileHeader> header =
+ aos::JsonToFlatbuffer<aos::logger::LogFileHeader>(header_json, &fbb);
+
+ fbb.FinishSizePrefixed(header);
+
+ const std::string orig_path = FLAGS_logfile + ".orig";
+ PCHECK(rename(FLAGS_logfile.c_str(), orig_path.c_str()) == 0);
+
+ aos::logger::SpanReader span_reader(orig_path);
+ CHECK(span_reader.ReadMessage().empty());
+
+ aos::logger::DetachedBufferWriter buffer_writer(FLAGS_logfile);
+ buffer_writer.QueueSizedFlatbuffer(&fbb);
+
+ while (true) {
+ absl::Span<const uint8_t> msg_data = span_reader.ReadMessage();
+ if (msg_data == absl::Span<const uint8_t>()) {
+ break;
+ }
+
+ buffer_writer.WriteSizedFlatbuffer(msg_data);
+ }
+ } else {
+ aos::logger::MessageReader reader(FLAGS_logfile);
+ aos::util::WriteStringToFileOrDie(
+ FLAGS_header, aos::FlatbufferToJson(reader.log_file_header(), true));
+ }
+ }
+
+ aos::Cleanup();
+ return 0;
+}
diff --git a/aos/events/logging/log_stats.cc b/aos/events/logging/log_stats.cc
index 65da871..88bf15c 100644
--- a/aos/events/logging/log_stats.cc
+++ b/aos/events/logging/log_stats.cc
@@ -14,6 +14,8 @@
name, "",
"Name to match for printing out channels. Empty means no name filter.");
+DEFINE_string(node, "", "Node to print stats out for.");
+
// define struct to hold all information
struct ChannelStats {
// pointer to the channel for which stats are collected
@@ -63,9 +65,23 @@
aos::SimulatedEventLoopFactory log_reader_factory(reader.configuration());
reader.Register(&log_reader_factory);
+ const aos::Node *node = nullptr;
+
+ if (aos::configuration::MultiNode(reader.configuration())) {
+ if (FLAGS_node.empty()) {
+ LOG(INFO) << "Need a --node specified. The log file has:";
+ for (const aos::Node *node : reader.Nodes()) {
+ LOG(INFO) << " " << node->name()->string_view();
+ }
+ return 1;
+ } else {
+ node = aos::configuration::GetNode(reader.configuration(), FLAGS_node);
+ }
+ }
+
// Make an eventloop for retrieving stats
std::unique_ptr<aos::EventLoop> stats_event_loop =
- log_reader_factory.MakeEventLoop("logstats", reader.node());
+ log_reader_factory.MakeEventLoop("logstats", node);
stats_event_loop->SkipTimingReport();
stats_event_loop->SkipAosLog();
diff --git a/aos/events/logging/logfile_utils.cc b/aos/events/logging/logfile_utils.cc
index e55b80d..03b89d1 100644
--- a/aos/events/logging/logfile_utils.cc
+++ b/aos/events/logging/logfile_utils.cc
@@ -11,6 +11,7 @@
#include "aos/configuration.h"
#include "aos/events/logging/logger_generated.h"
#include "aos/flatbuffer_merge.h"
+#include "aos/util/file.h"
#include "flatbuffers/flatbuffers.h"
#include "gflags/gflags.h"
#include "glog/logging.h"
@@ -24,9 +25,12 @@
namespace chrono = std::chrono;
DetachedBufferWriter::DetachedBufferWriter(std::string_view filename)
- : fd_(open(std::string(filename).c_str(),
- O_RDWR | O_CLOEXEC | O_CREAT | O_EXCL, 0774)) {
- PCHECK(fd_ != -1) << ": Failed to open " << filename;
+ : filename_(filename) {
+ util::MkdirP(filename, 0777);
+ fd_ = open(std::string(filename).c_str(),
+ O_RDWR | O_CLOEXEC | O_CREAT | O_EXCL, 0774);
+ VLOG(1) << "Opened " << filename << " for writing";
+ PCHECK(fd_ != -1) << ": Failed to open " << filename << " for writing";
}
DetachedBufferWriter::~DetachedBufferWriter() {
@@ -39,6 +43,26 @@
QueueSizedFlatbuffer(fbb->Release());
}
+void DetachedBufferWriter::WriteSizedFlatbuffer(
+ absl::Span<const uint8_t> span) {
+ // Cheat aggressively... Write out the queued up data, and then write this
+ // data once without buffering. It is hard to make a DetachedBuffer out of
+ // this data, and we don't want to worry about lifetimes.
+ Flush();
+ iovec_.clear();
+ iovec_.reserve(1);
+
+ struct iovec n;
+ n.iov_base = const_cast<uint8_t *>(span.data());
+ n.iov_len = span.size();
+ iovec_.emplace_back(n);
+
+ const ssize_t written = writev(fd_, iovec_.data(), iovec_.size());
+
+ PCHECK(written == static_cast<ssize_t>(n.iov_len))
+ << ": Wrote " << written << " expected " << n.iov_len;
+}
+
void DetachedBufferWriter::QueueSizedFlatbuffer(
flatbuffers::DetachedBuffer &&buffer) {
queued_size_ += buffer.size();
@@ -85,6 +109,7 @@
switch (log_type) {
case LogType::kLogMessage:
case LogType::kLogMessageAndDeliveryTime:
+ case LogType::kLogRemoteMessage:
data_offset =
fbb->CreateVector(static_cast<uint8_t *>(context.data), context.size);
break;
@@ -95,14 +120,30 @@
MessageHeader::Builder message_header_builder(*fbb);
message_header_builder.add_channel_index(channel_index);
- message_header_builder.add_queue_index(context.queue_index);
- message_header_builder.add_monotonic_sent_time(
- context.monotonic_event_time.time_since_epoch().count());
- message_header_builder.add_realtime_sent_time(
- context.realtime_event_time.time_since_epoch().count());
+
+ switch (log_type) {
+ case LogType::kLogRemoteMessage:
+ message_header_builder.add_queue_index(context.remote_queue_index);
+ message_header_builder.add_monotonic_sent_time(
+ context.monotonic_remote_time.time_since_epoch().count());
+ message_header_builder.add_realtime_sent_time(
+ context.realtime_remote_time.time_since_epoch().count());
+ break;
+
+ case LogType::kLogMessage:
+ case LogType::kLogMessageAndDeliveryTime:
+ case LogType::kLogDeliveryTimeOnly:
+ message_header_builder.add_queue_index(context.queue_index);
+ message_header_builder.add_monotonic_sent_time(
+ context.monotonic_event_time.time_since_epoch().count());
+ message_header_builder.add_realtime_sent_time(
+ context.realtime_event_time.time_since_epoch().count());
+ break;
+ }
switch (log_type) {
case LogType::kLogMessage:
+ case LogType::kLogRemoteMessage:
message_header_builder.add_data(data_offset);
break;
@@ -123,7 +164,8 @@
}
SpanReader::SpanReader(std::string_view filename)
- : fd_(open(std::string(filename).c_str(), O_RDONLY | O_CLOEXEC)) {
+ : filename_(filename),
+ fd_(open(std::string(filename).c_str(), O_RDONLY | O_CLOEXEC)) {
PCHECK(fd_ != -1) << ": Failed to open " << filename;
}
@@ -204,6 +246,20 @@
return true;
}
+FlatbufferVector<LogFileHeader> ReadHeader(std::string_view filename) {
+ SpanReader span_reader(filename);
+ // Make sure we have enough to read the size.
+ absl::Span<const uint8_t> config_data = span_reader.ReadMessage();
+
+ // Make sure something was read.
+ CHECK(config_data != absl::Span<const uint8_t>());
+
+ // And copy the config so we have it forever.
+ std::vector<uint8_t> data(
+ config_data.begin() + sizeof(flatbuffers::uoffset_t), config_data.end());
+ return FlatbufferVector<LogFileHeader>(std::move(data));
+}
+
MessageReader::MessageReader(std::string_view filename)
: span_reader_(filename) {
// Make sure we have enough to read the size.
@@ -215,9 +271,11 @@
// And copy the config so we have it forever.
configuration_ = std::vector<uint8_t>(config_data.begin(), config_data.end());
- max_out_of_order_duration_ = std::chrono::nanoseconds(
- flatbuffers::GetSizePrefixedRoot<LogFileHeader>(configuration_.data())
- ->max_out_of_order_duration());
+ max_out_of_order_duration_ =
+ std::chrono::nanoseconds(log_file_header()->max_out_of_order_duration());
+
+ VLOG(1) << "Opened " << filename << " as node "
+ << FlatbufferToJson(log_file_header()->node());
}
std::optional<FlatbufferVector<MessageHeader>> MessageReader::ReadMessage() {
@@ -233,23 +291,56 @@
chrono::nanoseconds(result.message().monotonic_sent_time()));
newest_timestamp_ = std::max(newest_timestamp_, timestamp);
- return result;
+ VLOG(1) << "Read from " << filename() << " data " << FlatbufferToJson(result);
+ return std::move(result);
}
-SortedMessageReader::SortedMessageReader(
+SplitMessageReader::SplitMessageReader(
const std::vector<std::string> &filenames)
: filenames_(filenames),
log_file_header_(FlatbufferDetachedBuffer<LogFileHeader>::Empty()) {
CHECK(NextLogFile()) << ": filenames is empty. Need files to read.";
+ // Grab any log file header. They should all match (and we will check as we
+ // open more of them).
log_file_header_ = CopyFlatBuffer(message_reader_->log_file_header());
+ // Setup per channel state.
channels_.resize(configuration()->channels()->size());
+ for (ChannelData &channel_data : channels_) {
+ channel_data.data.split_reader = this;
+ // Build up the timestamp list.
+ if (configuration::MultiNode(configuration())) {
+ channel_data.timestamps.resize(configuration()->nodes()->size());
+ for (MessageHeaderQueue &queue : channel_data.timestamps) {
+ queue.timestamps = true;
+ queue.split_reader = this;
+ }
+ }
+ }
- QueueMessages();
+ // Build up channels_to_write_ as an optimization to make it fast to figure
+ // out which datastructure to place any new data from a channel on.
+ for (const Channel *channel : *configuration()->channels()) {
+ // This is the main case. We will only see data on this node.
+ if (configuration::ChannelIsSendableOnNode(channel, node())) {
+ channels_to_write_.emplace_back(
+ &channels_[channels_to_write_.size()].data);
+ } else
+ // If we can't send, but can receive, we should be able to see
+ // timestamps here.
+ if (configuration::ChannelIsReadableOnNode(channel, node())) {
+ channels_to_write_.emplace_back(
+ &(channels_[channels_to_write_.size()]
+ .timestamps[configuration::GetNodeIndex(configuration(),
+ node())]));
+ } else {
+ channels_to_write_.emplace_back(nullptr);
+ }
+ }
}
-bool SortedMessageReader::NextLogFile() {
+bool SplitMessageReader::NextLogFile() {
if (next_filename_index_ == filenames_.size()) {
return false;
}
@@ -259,13 +350,8 @@
// We can't support the config diverging between two log file headers. See if
// they are the same.
if (next_filename_index_ != 0) {
- // Since we copied before, we need to copy again to guarantee that things
- // didn't get re-ordered.
- const FlatbufferDetachedBuffer<LogFileHeader> new_log_file_header =
- CopyFlatBuffer(message_reader_->log_file_header());
- CHECK_EQ(new_log_file_header.size(), log_file_header_.size());
- CHECK(memcmp(new_log_file_header.data(), log_file_header_.data(),
- log_file_header_.size()) == 0)
+ CHECK(CompareFlatBuffer(&log_file_header_.message(),
+ message_reader_->log_file_header()))
<< ": Header is different between log file chunks "
<< filenames_[next_filename_index_] << " and "
<< filenames_[next_filename_index_ - 1] << ", this is not supported.";
@@ -275,22 +361,245 @@
return true;
}
-void SortedMessageReader::EmplaceDataBack(
- FlatbufferVector<MessageHeader> &&new_data) {
- const monotonic_clock::time_point timestamp = monotonic_clock::time_point(
- chrono::nanoseconds(new_data.message().monotonic_sent_time()));
- const size_t channel_index = new_data.message().channel_index();
- CHECK_LT(channel_index, channels_.size());
+bool SplitMessageReader::QueueMessages(
+ monotonic_clock::time_point last_dequeued_time) {
+ // TODO(austin): Once we are happy that everything works, read a 256kb chunk
+ // to reduce the need to re-heap down below.
- if (channels_[channel_index].data.size() == 0) {
- channels_[channel_index].oldest_timestamp = timestamp;
- PushChannelHeap(timestamp, channel_index);
+ // Special case no more data. Otherwise we blow up on the CHECK statement
+ // confirming that we have enough data queued.
+ if (at_end_) {
+ return false;
}
- channels_[channel_index].data.emplace_back(std::move(new_data));
+
+ // If this isn't the first time around, confirm that we had enough data queued
+ // to follow the contract.
+ if (time_to_queue_ != monotonic_clock::min_time) {
+ CHECK_LE(last_dequeued_time,
+ newest_timestamp() - max_out_of_order_duration())
+ << " node " << FlatbufferToJson(node()) << " on " << this;
+
+ // Bail if there is enough data already queued.
+ if (last_dequeued_time < time_to_queue_) {
+ VLOG(1) << "All up to date on " << this << ", dequeued "
+ << last_dequeued_time << " queue time " << time_to_queue_;
+ return true;
+ }
+ } else {
+ // Startup takes a special dance. We want to queue up until the start time,
+ // but we then want to find the next message to read. The conservative
+ // answer is to immediately trigger a second requeue to get things moving.
+ time_to_queue_ = monotonic_start_time();
+ QueueMessages(time_to_queue_);
+ }
+
+ // If we are asked to queue, queue for at least max_out_of_order_duration past
+ // the last known time in the log file (ie the newest timestep read). As long
+ // as we requeue exactly when time_to_queue_ is dequeued and go no further, we
+ // are safe. And since we pop in order, that works.
+ //
+ // Special case the start of the log file. There should be at most 1 message
+ // from each channel at the start of the log file. So always force the start
+ // of the log file to just be read.
+ time_to_queue_ = std::max(time_to_queue_, newest_timestamp());
+ VLOG(1) << "Queueing, going until " << time_to_queue_ << " " << filename();
+
+ bool was_emplaced = false;
+ while (true) {
+ // Stop if we have enough.
+ if (newest_timestamp() >
+ time_to_queue_ + max_out_of_order_duration() &&
+ was_emplaced) {
+ VLOG(1) << "Done queueing on " << this << ", queued to "
+ << newest_timestamp() << " with requeue time " << time_to_queue_;
+ return true;
+ }
+
+ if (std::optional<FlatbufferVector<MessageHeader>> msg =
+ message_reader_->ReadMessage()) {
+ const MessageHeader &header = msg.value().message();
+
+ const monotonic_clock::time_point timestamp = monotonic_clock::time_point(
+ chrono::nanoseconds(header.monotonic_sent_time()));
+
+ VLOG(1) << "Queued " << this << " " << filename()
+ << " ttq: " << time_to_queue_ << " now "
+ << newest_timestamp() << " start time "
+ << monotonic_start_time() << " " << FlatbufferToJson(&header);
+
+ const int channel_index = header.channel_index();
+ was_emplaced = channels_to_write_[channel_index]->emplace_back(
+ std::move(msg.value()));
+ if (was_emplaced) {
+ newest_timestamp_ = std::max(newest_timestamp_, timestamp);
+ }
+ } else {
+ if (!NextLogFile()) {
+ VLOG(1) << "End of log file.";
+ at_end_ = true;
+ return false;
+ }
+ }
+ }
+}
+
+void SplitMessageReader::SetTimestampMerger(TimestampMerger *timestamp_merger,
+ int channel_index,
+ const Node *target_node) {
+ const Node *reinterpreted_target_node =
+ configuration::GetNodeOrDie(configuration(), target_node);
+ const Channel *const channel =
+ configuration()->channels()->Get(channel_index);
+
+ VLOG(1) << " Configuring merger " << this << " for channel " << channel_index
+ << " "
+ << configuration::CleanedChannelToString(
+ configuration()->channels()->Get(channel_index));
+
+ MessageHeaderQueue *message_header_queue = nullptr;
+
+ // Figure out if this log file is from our point of view, or the other node's
+ // point of view.
+ if (node() == reinterpreted_target_node) {
+ VLOG(1) << " Replaying as logged node " << filename();
+
+ if (configuration::ChannelIsSendableOnNode(channel, node())) {
+ VLOG(1) << " Data on node";
+ message_header_queue = &(channels_[channel_index].data);
+ } else if (configuration::ChannelIsReadableOnNode(channel, node())) {
+ VLOG(1) << " Timestamps on node";
+ message_header_queue =
+ &(channels_[channel_index].timestamps[configuration::GetNodeIndex(
+ configuration(), node())]);
+ } else {
+ VLOG(1) << " Dropping";
+ }
+ } else {
+ VLOG(1) << " Replaying as other node " << filename();
+ // We are replaying from another node's point of view. The only interesting
+ // data is data that is sent from our node and received on theirs.
+ if (configuration::ChannelIsReadableOnNode(channel,
+ reinterpreted_target_node) &&
+ configuration::ChannelIsSendableOnNode(channel, node())) {
+ VLOG(1) << " Readable on target node";
+ // Data from another node.
+ message_header_queue = &(channels_[channel_index].data);
+ } else {
+ VLOG(1) << " Dropping";
+ // This is either not sendable on the other node, or is a timestamp and
+ // therefore not interesting.
+ }
+ }
+
+ // If we found one, write it down. This will be nullptr when there is nothing
+ // relevant on this channel on this node for the target node. In that case,
+ // we want to drop the message instead of queueing it.
+ if (message_header_queue != nullptr) {
+ message_header_queue->timestamp_merger = timestamp_merger;
+ }
+}
+
+std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+SplitMessageReader::PopOldest(int channel_index) {
+ CHECK_GT(channels_[channel_index].data.size(), 0u);
+ const std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ timestamp = channels_[channel_index].data.front_timestamp();
+ FlatbufferVector<MessageHeader> front =
+ std::move(channels_[channel_index].data.front());
+ channels_[channel_index].data.pop_front();
+
+ VLOG(1) << "Popped " << this << " " << std::get<0>(timestamp);
+
+ QueueMessages(std::get<0>(timestamp));
+
+ return std::make_tuple(std::get<0>(timestamp), std::get<1>(timestamp),
+ std::move(front));
+}
+
+std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+SplitMessageReader::PopOldest(int channel, int node_index) {
+ CHECK_GT(channels_[channel].timestamps[node_index].size(), 0u);
+ const std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ timestamp = channels_[channel].timestamps[node_index].front_timestamp();
+ FlatbufferVector<MessageHeader> front =
+ std::move(channels_[channel].timestamps[node_index].front());
+ channels_[channel].timestamps[node_index].pop_front();
+
+ VLOG(1) << "Popped " << this << " " << std::get<0>(timestamp);
+
+ QueueMessages(std::get<0>(timestamp));
+
+ return std::make_tuple(std::get<0>(timestamp), std::get<1>(timestamp),
+ std::move(front));
+}
+
+bool SplitMessageReader::MessageHeaderQueue::emplace_back(
+ FlatbufferVector<MessageHeader> &&msg) {
+ CHECK(split_reader != nullptr);
+
+ // If there is no timestamp merger for this queue, nobody is listening. Drop
+ // the message. This happens when a log file from another node is replayed,
+ // and the timestamp mergers down stream just don't care.
+ if (timestamp_merger == nullptr) {
+ return false;
+ }
+
+ CHECK(timestamps != msg.message().has_data())
+ << ": Got timestamps and data mixed up on a node. "
+ << FlatbufferToJson(msg);
+
+ data_.emplace_back(std::move(msg));
+
+ if (data_.size() == 1u) {
+ // Yup, new data. Notify.
+ if (timestamps) {
+ timestamp_merger->UpdateTimestamp(split_reader, front_timestamp());
+ } else {
+ timestamp_merger->Update(split_reader, front_timestamp());
+ }
+ }
+
+ return true;
+}
+
+void SplitMessageReader::MessageHeaderQueue::pop_front() {
+ data_.pop_front();
+ if (data_.size() != 0u) {
+ // Yup, new data.
+ if (timestamps) {
+ timestamp_merger->UpdateTimestamp(split_reader, front_timestamp());
+ } else {
+ timestamp_merger->Update(split_reader, front_timestamp());
+ }
+ }
}
namespace {
+bool SplitMessageReaderHeapCompare(
+ const std::tuple<monotonic_clock::time_point, uint32_t,
+ SplitMessageReader *>
+ first,
+ const std::tuple<monotonic_clock::time_point, uint32_t,
+ SplitMessageReader *>
+ second) {
+ if (std::get<0>(first) > std::get<0>(second)) {
+ return true;
+ } else if (std::get<0>(first) == std::get<0>(second)) {
+ if (std::get<1>(first) > std::get<1>(second)) {
+ return true;
+ } else if (std::get<1>(first) == std::get<1>(second)) {
+ return std::get<2>(first) > std::get<2>(second);
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
bool ChannelHeapCompare(
const std::pair<monotonic_clock::time_point, int> first,
const std::pair<monotonic_clock::time_point, int> second) {
@@ -305,8 +614,403 @@
} // namespace
-void SortedMessageReader::PushChannelHeap(monotonic_clock::time_point timestamp,
- int channel_index) {
+TimestampMerger::TimestampMerger(
+ const Configuration *configuration,
+ std::vector<SplitMessageReader *> split_message_readers, int channel_index,
+ const Node *target_node, ChannelMerger *channel_merger)
+ : configuration_(configuration),
+ split_message_readers_(std::move(split_message_readers)),
+ channel_index_(channel_index),
+ node_index_(configuration::MultiNode(configuration)
+ ? configuration::GetNodeIndex(configuration, target_node)
+ : -1),
+ channel_merger_(channel_merger) {
+ // Tell the readers we care so they know who to notify.
+ VLOG(1) << "Configuring channel " << channel_index << " target node "
+ << FlatbufferToJson(target_node);
+ for (SplitMessageReader *reader : split_message_readers_) {
+ reader->SetTimestampMerger(this, channel_index, target_node);
+ }
+
+ // And then determine if we need to track timestamps.
+ const Channel *channel = configuration->channels()->Get(channel_index);
+ if (!configuration::ChannelIsSendableOnNode(channel, target_node) &&
+ configuration::ChannelIsReadableOnNode(channel, target_node)) {
+ has_timestamps_ = true;
+ }
+}
+
+void TimestampMerger::PushMessageHeap(
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ timestamp,
+ SplitMessageReader *split_message_reader) {
+ DCHECK(std::find_if(message_heap_.begin(), message_heap_.end(),
+ [split_message_reader](
+ const std::tuple<monotonic_clock::time_point,
+ uint32_t, SplitMessageReader *>
+ x) {
+ return std::get<2>(x) == split_message_reader;
+ }) == message_heap_.end())
+ << ": Pushing message when it is already in the heap.";
+
+ message_heap_.push_back(std::make_tuple(
+ std::get<0>(timestamp), std::get<1>(timestamp), split_message_reader));
+
+ std::push_heap(message_heap_.begin(), message_heap_.end(),
+ &SplitMessageReaderHeapCompare);
+
+ // If we are just a data merger, don't wait for timestamps.
+ if (!has_timestamps_) {
+ channel_merger_->Update(std::get<0>(timestamp), channel_index_);
+ pushed_ = true;
+ }
+}
+
+std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+TimestampMerger::oldest_message() const {
+ CHECK_GT(message_heap_.size(), 0u);
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ oldest_message_reader = message_heap_.front();
+ return std::get<2>(oldest_message_reader)->oldest_message(channel_index_);
+}
+
+std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+TimestampMerger::oldest_timestamp() const {
+ CHECK_GT(timestamp_heap_.size(), 0u);
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ oldest_message_reader = timestamp_heap_.front();
+ return std::get<2>(oldest_message_reader)
+ ->oldest_message(channel_index_, node_index_);
+}
+
+void TimestampMerger::PushTimestampHeap(
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ timestamp,
+ SplitMessageReader *split_message_reader) {
+ DCHECK(std::find_if(timestamp_heap_.begin(), timestamp_heap_.end(),
+ [split_message_reader](
+ const std::tuple<monotonic_clock::time_point,
+ uint32_t, SplitMessageReader *>
+ x) {
+ return std::get<2>(x) == split_message_reader;
+ }) == timestamp_heap_.end())
+ << ": Pushing timestamp when it is already in the heap.";
+
+ timestamp_heap_.push_back(std::make_tuple(
+ std::get<0>(timestamp), std::get<1>(timestamp), split_message_reader));
+
+ std::push_heap(timestamp_heap_.begin(), timestamp_heap_.end(),
+ SplitMessageReaderHeapCompare);
+
+ // If we are a timestamp merger, don't wait for data. Missing data will be
+ // caught at read time.
+ if (has_timestamps_) {
+ channel_merger_->Update(std::get<0>(timestamp), channel_index_);
+ pushed_ = true;
+ }
+}
+
+std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+TimestampMerger::PopMessageHeap() {
+ // Pop the oldest message reader pointer off the heap.
+ CHECK_GT(message_heap_.size(), 0u);
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ oldest_message_reader = message_heap_.front();
+
+ std::pop_heap(message_heap_.begin(), message_heap_.end(),
+ &SplitMessageReaderHeapCompare);
+ message_heap_.pop_back();
+
+ // Pop the oldest message. This re-pushes any messages from the reader to the
+ // message heap.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ oldest_message =
+ std::get<2>(oldest_message_reader)->PopOldest(channel_index_);
+
+ // Confirm that the time and queue_index we have recorded matches.
+ CHECK_EQ(std::get<0>(oldest_message), std::get<0>(oldest_message_reader));
+ CHECK_EQ(std::get<1>(oldest_message), std::get<1>(oldest_message_reader));
+
+ // Now, keep reading until we have found all duplicates.
+ while (message_heap_.size() > 0u) {
+ // See if it is a duplicate.
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ next_oldest_message_reader = message_heap_.front();
+
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ next_oldest_message_time = std::get<2>(next_oldest_message_reader)
+ ->oldest_message(channel_index_);
+
+ if (std::get<0>(next_oldest_message_time) == std::get<0>(oldest_message) &&
+ std::get<1>(next_oldest_message_time) == std::get<1>(oldest_message)) {
+ // Pop the message reader pointer.
+ std::pop_heap(message_heap_.begin(), message_heap_.end(),
+ &SplitMessageReaderHeapCompare);
+ message_heap_.pop_back();
+
+ // Pop the next oldest message. This re-pushes any messages from the
+ // reader.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ next_oldest_message = std::get<2>(next_oldest_message_reader)
+ ->PopOldest(channel_index_);
+
+ // And make sure the message matches in it's entirety.
+ CHECK(std::get<2>(oldest_message).span() ==
+ std::get<2>(next_oldest_message).span())
+ << ": Data at the same timestamp doesn't match.";
+ } else {
+ break;
+ }
+ }
+
+ return oldest_message;
+}
+
+std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+TimestampMerger::PopTimestampHeap() {
+ // Pop the oldest message reader pointer off the heap.
+ CHECK_GT(timestamp_heap_.size(), 0u);
+
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ oldest_timestamp_reader = timestamp_heap_.front();
+
+ std::pop_heap(timestamp_heap_.begin(), timestamp_heap_.end(),
+ &SplitMessageReaderHeapCompare);
+ timestamp_heap_.pop_back();
+
+ CHECK(node_index_ != -1) << ": Timestamps in a single node environment";
+
+ // Pop the oldest message. This re-pushes any timestamps from the reader to
+ // the timestamp heap.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ oldest_timestamp = std::get<2>(oldest_timestamp_reader)
+ ->PopOldest(channel_index_, node_index_);
+
+ // Confirm that the time we have recorded matches.
+ CHECK_EQ(std::get<0>(oldest_timestamp), std::get<0>(oldest_timestamp_reader));
+ CHECK_EQ(std::get<1>(oldest_timestamp), std::get<1>(oldest_timestamp_reader));
+
+ // TODO(austin): What if we get duplicate timestamps?
+
+ return oldest_timestamp;
+}
+
+std::tuple<TimestampMerger::DeliveryTimestamp, FlatbufferVector<MessageHeader>>
+TimestampMerger::PopOldest() {
+ if (has_timestamps_) {
+ CHECK_GT(message_heap_.size(), 0u)
+ << ": Missing data from source node, no data available to match "
+ "timestamp on "
+ << configuration::CleanedChannelToString(
+ configuration_->channels()->Get(channel_index_));
+
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ oldest_timestamp = PopTimestampHeap();
+
+ TimestampMerger::DeliveryTimestamp timestamp;
+ timestamp.monotonic_event_time =
+ monotonic_clock::time_point(chrono::nanoseconds(
+ std::get<2>(oldest_timestamp).message().monotonic_sent_time()));
+ timestamp.realtime_event_time =
+ realtime_clock::time_point(chrono::nanoseconds(
+ std::get<2>(oldest_timestamp).message().realtime_sent_time()));
+
+ // Consistency check.
+ CHECK_EQ(timestamp.monotonic_event_time, std::get<0>(oldest_timestamp));
+ CHECK_EQ(std::get<2>(oldest_timestamp).message().queue_index(),
+ std::get<1>(oldest_timestamp));
+
+ monotonic_clock::time_point remote_timestamp_monotonic_time(
+ chrono::nanoseconds(
+ std::get<2>(oldest_timestamp).message().monotonic_remote_time()));
+
+ while (true) {
+ {
+ // Ok, now try grabbing data until we find one which matches.
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_message_ref = oldest_message();
+
+ // Time at which the message was sent (this message is written from the
+ // sending node's perspective.
+ monotonic_clock::time_point remote_monotonic_time(chrono::nanoseconds(
+ std::get<2>(oldest_message_ref)->monotonic_sent_time()));
+
+ if (remote_monotonic_time < remote_timestamp_monotonic_time) {
+ LOG(INFO) << "Undelivered message, skipping. Remote time is "
+ << remote_monotonic_time << " timestamp is "
+ << remote_timestamp_monotonic_time << " on channel "
+ << channel_index_;
+ PopMessageHeap();
+ continue;
+ } else if (remote_monotonic_time > remote_timestamp_monotonic_time) {
+ LOG(INFO) << "Data not found. Remote time should be "
+ << remote_timestamp_monotonic_time << " on channel "
+ << channel_index_;
+ return std::make_tuple(timestamp,
+ std::move(std::get<2>(oldest_timestamp)));
+ }
+
+ timestamp.monotonic_remote_time = remote_monotonic_time;
+ }
+
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ oldest_message = PopMessageHeap();
+
+ timestamp.realtime_remote_time =
+ realtime_clock::time_point(chrono::nanoseconds(
+ std::get<2>(oldest_message).message().realtime_sent_time()));
+ timestamp.remote_queue_index =
+ std::get<2>(oldest_message).message().queue_index();
+
+ CHECK_EQ(timestamp.monotonic_remote_time,
+ remote_timestamp_monotonic_time);
+
+ CHECK_EQ(timestamp.remote_queue_index,
+ std::get<2>(oldest_timestamp).message().remote_queue_index())
+ << ": " << FlatbufferToJson(&std::get<2>(oldest_timestamp).message())
+ << " data "
+ << FlatbufferToJson(&std::get<2>(oldest_message).message());
+
+ return std::make_tuple(timestamp, std::get<2>(oldest_message));
+ }
+ } else {
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ oldest_message = PopMessageHeap();
+
+ TimestampMerger::DeliveryTimestamp timestamp;
+ timestamp.monotonic_event_time =
+ monotonic_clock::time_point(chrono::nanoseconds(
+ std::get<2>(oldest_message).message().monotonic_sent_time()));
+ timestamp.realtime_event_time =
+ realtime_clock::time_point(chrono::nanoseconds(
+ std::get<2>(oldest_message).message().realtime_sent_time()));
+ timestamp.remote_queue_index = 0xffffffff;
+
+ CHECK_EQ(std::get<0>(oldest_message), timestamp.monotonic_event_time);
+ CHECK_EQ(std::get<1>(oldest_message),
+ std::get<2>(oldest_message).message().queue_index());
+
+ return std::make_tuple(timestamp, std::get<2>(oldest_message));
+ }
+}
+
+namespace {
+std::vector<std::unique_ptr<SplitMessageReader>> MakeSplitMessageReaders(
+ const std::vector<std::vector<std::string>> &filenames) {
+ CHECK_GT(filenames.size(), 0u);
+ // Build up all the SplitMessageReaders.
+ std::vector<std::unique_ptr<SplitMessageReader>> result;
+ for (const std::vector<std::string> &filenames : filenames) {
+ result.emplace_back(std::make_unique<SplitMessageReader>(filenames));
+ }
+ return result;
+}
+} // namespace
+
+ChannelMerger::ChannelMerger(
+ const std::vector<std::vector<std::string>> &filenames)
+ : split_message_readers_(MakeSplitMessageReaders(filenames)),
+ log_file_header_(
+ CopyFlatBuffer(split_message_readers_[0]->log_file_header())) {
+ // Now, confirm that the configuration matches for each and pick a start time.
+ // Also return the list of possible nodes.
+ for (const std::unique_ptr<SplitMessageReader> &reader :
+ split_message_readers_) {
+ CHECK(CompareFlatBuffer(log_file_header_.message().configuration(),
+ reader->log_file_header()->configuration()))
+ << ": Replaying log files with different configurations isn't "
+ "supported";
+ }
+
+ nodes_ = configuration::GetNodes(configuration());
+}
+
+bool ChannelMerger::SetNode(const Node *target_node) {
+ std::vector<SplitMessageReader *> split_message_readers;
+ for (const std::unique_ptr<SplitMessageReader> &reader :
+ split_message_readers_) {
+ split_message_readers.emplace_back(reader.get());
+ }
+
+ // Go find a log_file_header for this node.
+ {
+ bool found_node = false;
+
+ for (const std::unique_ptr<SplitMessageReader> &reader :
+ split_message_readers_) {
+ if (CompareFlatBuffer(reader->node(), target_node)) {
+ if (!found_node) {
+ found_node = true;
+ log_file_header_ = CopyFlatBuffer(reader->log_file_header());
+ VLOG(1) << "Found log file " << reader->filename() << " with node "
+ << FlatbufferToJson(reader->node()) << " start_time "
+ << monotonic_start_time();
+ } else {
+ // And then make sure all the other files have matching headers.
+ CHECK(CompareFlatBuffer(log_file_header(), reader->log_file_header()))
+ << ": " << FlatbufferToJson(log_file_header()) << " reader "
+ << FlatbufferToJson(reader->log_file_header());
+ }
+ }
+ }
+
+ if (!found_node) {
+ LOG(WARNING) << "Failed to find log file for node "
+ << FlatbufferToJson(target_node);
+ return false;
+ }
+ }
+
+ // Build up all the timestamp mergers. This connects up all the
+ // SplitMessageReaders.
+ timestamp_mergers_.reserve(configuration()->channels()->size());
+ for (size_t channel_index = 0;
+ channel_index < configuration()->channels()->size(); ++channel_index) {
+ timestamp_mergers_.emplace_back(
+ configuration(), split_message_readers, channel_index,
+ configuration::GetNode(configuration(), target_node), this);
+ }
+
+ // And prime everything.
+ for (std::unique_ptr<SplitMessageReader> &split_message_reader :
+ split_message_readers_) {
+ split_message_reader->QueueMessages(
+ split_message_reader->monotonic_start_time());
+ }
+
+ node_ = configuration::GetNodeOrDie(configuration(), target_node);
+ return true;
+}
+
+monotonic_clock::time_point ChannelMerger::OldestMessage() const {
+ if (channel_heap_.size() == 0u) {
+ return monotonic_clock::max_time;
+ }
+ return channel_heap_.front().first;
+}
+
+void ChannelMerger::PushChannelHeap(monotonic_clock::time_point timestamp,
+ int channel_index) {
+ // Pop and recreate the heap if it has already been pushed. And since we are
+ // pushing again, we don't need to clear pushed.
+ if (timestamp_mergers_[channel_index].pushed()) {
+ channel_heap_.erase(std::find_if(
+ channel_heap_.begin(), channel_heap_.end(),
+ [channel_index](const std::pair<monotonic_clock::time_point, int> x) {
+ return x.second == channel_index;
+ }));
+ std::make_heap(channel_heap_.begin(), channel_heap_.end(),
+ ChannelHeapCompare);
+ }
+
channel_heap_.push_back(std::make_pair(timestamp, channel_index));
// The default sort puts the newest message first. Use a custom comparator to
@@ -315,60 +1019,152 @@
ChannelHeapCompare);
}
-void SortedMessageReader::QueueMessages() {
- while (true) {
- // Don't queue if we have enough data already.
- // When a log file starts, there should be a message from each channel.
- // Those messages might be very old. Make sure to read a chunk past the
- // starting time.
- if (channel_heap_.size() > 0 &&
- message_reader_->newest_timestamp() >
- std::max(oldest_message().first, monotonic_start_time()) +
- message_reader_->max_out_of_order_duration()) {
- break;
- }
-
- if (std::optional<FlatbufferVector<MessageHeader>> msg =
- message_reader_->ReadMessage()) {
- EmplaceDataBack(std::move(msg.value()));
- } else {
- if (!NextLogFile()) {
- break;
- }
- }
- }
-}
-
-std::tuple<monotonic_clock::time_point, int, FlatbufferVector<MessageHeader>>
-SortedMessageReader::PopOldestChannel() {
+std::tuple<TimestampMerger::DeliveryTimestamp, int,
+ FlatbufferVector<MessageHeader>>
+ChannelMerger::PopOldest() {
+ CHECK(channel_heap_.size() > 0);
std::pair<monotonic_clock::time_point, int> oldest_channel_data =
channel_heap_.front();
+ int channel_index = oldest_channel_data.second;
std::pop_heap(channel_heap_.begin(), channel_heap_.end(),
&ChannelHeapCompare);
channel_heap_.pop_back();
+ timestamp_mergers_[channel_index].set_pushed(false);
- struct ChannelData &channel = channels_[oldest_channel_data.second];
+ TimestampMerger *merger = ×tamp_mergers_[channel_index];
- FlatbufferVector<MessageHeader> front = std::move(channel.front());
+ // Merger handles any queueing needed from here.
+ std::tuple<TimestampMerger::DeliveryTimestamp,
+ FlatbufferVector<MessageHeader>>
+ message = merger->PopOldest();
- channel.data.pop_front();
+ return std::make_tuple(std::get<0>(message), channel_index,
+ std::move(std::get<1>(message)));
+}
- // Re-push it and update the oldest timestamp.
- if (channel.data.size() != 0) {
- const monotonic_clock::time_point timestamp = monotonic_clock::time_point(
- chrono::nanoseconds(channel.front().message().monotonic_sent_time()));
- PushChannelHeap(timestamp, oldest_channel_data.second);
- channel.oldest_timestamp = timestamp;
- } else {
- channel.oldest_timestamp = monotonic_clock::min_time;
+std::string SplitMessageReader::MessageHeaderQueue::DebugString() const {
+ std::stringstream ss;
+ for (size_t i = 0; i < data_.size(); ++i) {
+ if (timestamps) {
+ ss << " msg: ";
+ } else {
+ ss << " timestamp: ";
+ }
+ ss << monotonic_clock::time_point(std::chrono::nanoseconds(
+ data_[i].message().monotonic_sent_time()))
+ << " ("
+ << realtime_clock::time_point(
+ std::chrono::nanoseconds(data_[i].message().realtime_sent_time()))
+ << ") " << data_[i].message().queue_index();
+ if (timestamps) {
+ ss << " <- remote "
+ << monotonic_clock::time_point(std::chrono::nanoseconds(
+ data_[i].message().monotonic_remote_time()))
+ << " ("
+ << realtime_clock::time_point(std::chrono::nanoseconds(
+ data_[i].message().realtime_remote_time()))
+ << ")";
+ }
+ ss << "\n";
}
- if (oldest_channel_data.first > message_reader_->queue_data_time()) {
- QueueMessages();
+ return ss.str();
+}
+
+std::string SplitMessageReader::DebugString(int channel) const {
+ std::stringstream ss;
+ ss << "[\n";
+ ss << channels_[channel].data.DebugString();
+ ss << " ]";
+ return ss.str();
+}
+
+std::string SplitMessageReader::DebugString(int channel, int node_index) const {
+ std::stringstream ss;
+ ss << "[\n";
+ ss << channels_[channel].timestamps[node_index].DebugString();
+ ss << " ]";
+ return ss.str();
+}
+
+std::string TimestampMerger::DebugString() const {
+ std::stringstream ss;
+
+ if (timestamp_heap_.size() > 0) {
+ ss << " timestamp_heap {\n";
+ std::vector<
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>>
+ timestamp_heap = timestamp_heap_;
+ while (timestamp_heap.size() > 0u) {
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ oldest_timestamp_reader = timestamp_heap.front();
+
+ ss << " " << std::get<2>(oldest_timestamp_reader) << " "
+ << std::get<0>(oldest_timestamp_reader) << " queue_index ("
+ << std::get<1>(oldest_timestamp_reader) << ") ttq "
+ << std::get<2>(oldest_timestamp_reader)->time_to_queue() << " "
+ << std::get<2>(oldest_timestamp_reader)->filename() << " -> "
+ << std::get<2>(oldest_timestamp_reader)
+ ->DebugString(channel_index_, node_index_)
+ << "\n";
+
+ std::pop_heap(timestamp_heap.begin(), timestamp_heap.end(),
+ &SplitMessageReaderHeapCompare);
+ timestamp_heap.pop_back();
+ }
+ ss << " }\n";
}
- return std::make_tuple(oldest_channel_data.first, oldest_channel_data.second,
- std::move(front));
+ ss << " message_heap {\n";
+ {
+ std::vector<
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>>
+ message_heap = message_heap_;
+ while (message_heap.size() > 0u) {
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>
+ oldest_message_reader = message_heap.front();
+
+ ss << " " << std::get<2>(oldest_message_reader) << " "
+ << std::get<0>(oldest_message_reader) << " queue_index ("
+ << std::get<1>(oldest_message_reader) << ") ttq "
+ << std::get<2>(oldest_message_reader)->time_to_queue() << " "
+ << std::get<2>(oldest_message_reader)->filename() << " -> "
+ << std::get<2>(oldest_message_reader)->DebugString(channel_index_)
+ << "\n";
+
+ std::pop_heap(message_heap.begin(), message_heap.end(),
+ &SplitMessageReaderHeapCompare);
+ message_heap.pop_back();
+ }
+ }
+ ss << " }";
+
+ return ss.str();
+}
+
+std::string ChannelMerger::DebugString() const {
+ std::stringstream ss;
+ ss << "start_time " << realtime_start_time() << " " << monotonic_start_time()
+ << "\n";
+ ss << "channel_heap {\n";
+ std::vector<std::pair<monotonic_clock::time_point, int>> channel_heap =
+ channel_heap_;
+ while (channel_heap.size() > 0u) {
+ std::tuple<monotonic_clock::time_point, int> channel = channel_heap.front();
+ ss << " " << std::get<0>(channel) << " (" << std::get<1>(channel) << ") "
+ << configuration::CleanedChannelToString(
+ configuration()->channels()->Get(std::get<1>(channel)))
+ << "\n";
+
+ ss << timestamp_mergers_[std::get<1>(channel)].DebugString() << "\n";
+
+ std::pop_heap(channel_heap.begin(), channel_heap.end(),
+ &ChannelHeapCompare);
+ channel_heap.pop_back();
+ }
+ ss << "}";
+
+ return ss.str();
}
} // namespace logger
diff --git a/aos/events/logging/logfile_utils.h b/aos/events/logging/logfile_utils.h
index 6b8e9aa..e5e0175 100644
--- a/aos/events/logging/logfile_utils.h
+++ b/aos/events/logging/logfile_utils.h
@@ -26,10 +26,11 @@
// The message originated on another node. Log it and the delivery times
// together. The message_gateway is responsible for logging any messages
// which didn't get delivered.
- kLogMessageAndDeliveryTime
+ kLogMessageAndDeliveryTime,
+ // The message originated on the other node and should be logged on this node.
+ kLogRemoteMessage
};
-
// This class manages efficiently writing a sequence of detached buffers to a
// file. It queues them up and batches the write operation.
class DetachedBufferWriter {
@@ -37,6 +38,8 @@
DetachedBufferWriter(std::string_view filename);
~DetachedBufferWriter();
+ std::string_view filename() const { return filename_; }
+
// TODO(austin): Snappy compress the log file if it ends with .snappy!
// Queues up a finished FlatBufferBuilder to be written. Steals the detached
@@ -44,11 +47,15 @@
void QueueSizedFlatbuffer(flatbuffers::FlatBufferBuilder *fbb);
// Queues up a detached buffer directly.
void QueueSizedFlatbuffer(flatbuffers::DetachedBuffer &&buffer);
+ // Writes a Span. This is not terribly optimized right now.
+ void WriteSizedFlatbuffer(absl::Span<const uint8_t> span);
// Triggers data to be provided to the kernel and written.
void Flush();
private:
+ const std::string filename_;
+
int fd_ = -1;
// Size of all the data in the queue.
@@ -66,6 +73,8 @@
flatbuffers::FlatBufferBuilder *fbb, const Context &context,
int channel_index, LogType log_type);
+FlatbufferVector<LogFileHeader> ReadHeader(std::string_view filename);
+
// Class to read chunks out of a log file.
class SpanReader {
public:
@@ -73,6 +82,8 @@
~SpanReader() { close(fd_); }
+ std::string_view filename() const { return filename_; }
+
// Returns a span with the data for a message from the log file, excluding
// the size.
absl::Span<const uint8_t> ReadMessage();
@@ -90,6 +101,8 @@
// Reads a chunk of data into data_. Returns false if no data was read.
bool ReadBlock();
+ const std::string filename_;
+
// File descriptor for the log file.
int fd_ = -1;
@@ -136,6 +149,8 @@
public:
MessageReader(std::string_view filename);
+ std::string_view filename() const { return span_reader_.filename(); }
+
// Returns the header from the log file.
const LogFileHeader *log_file_header() const {
return flatbuffers::GetSizePrefixedRoot<LogFileHeader>(
@@ -148,6 +163,7 @@
return max_out_of_order_duration_;
}
+ // Returns the newest timestamp read out of the log file.
monotonic_clock::time_point newest_timestamp() const {
return newest_timestamp_;
}
@@ -175,52 +191,64 @@
monotonic_clock::time_point newest_timestamp_ = monotonic_clock::min_time;
};
-// We need to read a large chunk at a time, then kit it up into parts and
-// sort.
-//
-// We want to read 256 KB chunks at a time. This is the fastest read size.
-// This leaves us with a fragmentation problem though.
-//
-// The easy answer is to read 256 KB chunks. Then, malloc and memcpy those
-// chunks into single flatbuffer messages and manage them in a sorted queue.
-// Everything is copied three times (into 256 kb buffer, then into separate
-// buffer, then into sender), but none of it is all that expensive. We can
-// optimize if it is slow later.
-//
-// As we place the elements in the sorted list of times, keep doing this
-// until we read a message that is newer than the threshold.
-//
-// Then repeat. Keep filling up the sorted list with 256 KB chunks (need a
-// small state machine so we can resume), and keep pulling messages back out
-// and sending.
-//
-// For sorting, we want to use the fact that each channel is sorted, and
-// then merge sort the channels. Have a vector of deques, and then hold a
-// sorted list of pointers to those.
-class SortedMessageReader {
- public:
- SortedMessageReader(const std::vector<std::string> &filenames);
+class TimestampMerger;
- // Returns the header from the log file.
+// A design requirement is that the relevant data for a channel is not more than
+// max_out_of_order_duration out of order. We approach sorting in layers.
+//
+// 1) Split each (maybe chunked) log file into one queue per channel. Read this
+// log file looking for data pertaining to a specific node.
+// (SplitMessageReader)
+// 2) Merge all the data per channel from the different log files into a sorted
+// list of timestamps and messages. (TimestampMerger)
+// 3) Combine the timestamps and messages. (TimestampMerger)
+// 4) Merge all the channels to produce the next message on a node.
+// (ChannelMerger)
+// 5) Duplicate this entire stack per node.
+
+// This class splits messages and timestamps up into a queue per channel, and
+// handles reading data from multiple chunks.
+class SplitMessageReader {
+ public:
+ SplitMessageReader(const std::vector<std::string> &filenames);
+
+ // Sets the TimestampMerger that gets notified for each channel. The node
+ // that the TimestampMerger is merging as needs to be passed in.
+ void SetTimestampMerger(TimestampMerger *timestamp_merger, int channel,
+ const Node *target_node);
+
+ // Returns the (timestamp, queue_idex) for the oldest message in a channel, or
+ // max_time if there is nothing in the channel.
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_message(int channel) {
+ return channels_[channel].data.front_timestamp();
+ }
+
+ // Returns the (timestamp, queue_index) for the oldest delivery time in a
+ // channel, or max_time if there is nothing in the channel.
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_message(int channel, int destination_node) {
+ return channels_[channel].timestamps[destination_node].front_timestamp();
+ }
+
+ // Returns the timestamp, queue_index, and message for the oldest data on a
+ // channel. Requeues data as needed.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ PopOldest(int channel_index);
+
+ // Returns the timestamp, queue_index, and message for the oldest timestamp on
+ // a channel delivered to a node. Requeues data as needed.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ PopOldest(int channel, int node_index);
+
+ // Returns the header for the log files.
const LogFileHeader *log_file_header() const {
return &log_file_header_.message();
}
- // Returns a pointer to the channel with the oldest message in it, and the
- // timestamp.
- const std::pair<monotonic_clock::time_point, int> &oldest_message() const {
- return channel_heap_.front();
- }
-
- // Returns the number of channels with data still in them.
- size_t active_channel_count() const { return channel_heap_.size(); }
-
- // Returns the configuration from the log file header.
- const Configuration *configuration() const {
- return log_file_header()->configuration();
- }
-
- // Returns the start time on both the monotonic and realtime clocks.
+ // Returns the starting time for this set of log files.
monotonic_clock::time_point monotonic_start_time() {
return monotonic_clock::time_point(
std::chrono::nanoseconds(log_file_header()->monotonic_start_time()));
@@ -230,74 +258,338 @@
std::chrono::nanoseconds(log_file_header()->realtime_start_time()));
}
+ // Returns the configuration from the log file header.
+ const Configuration *configuration() const {
+ return log_file_header()->configuration();
+ }
+
// Returns the node who's point of view this log file is from. Make sure this
// is a pointer in the configuration() nodes list so it can be consumed
// elsewhere.
const Node *node() const {
if (configuration()->has_nodes()) {
- CHECK(log_file_header()->has_node());
- CHECK(log_file_header()->node()->has_name());
- return configuration::GetNode(
- configuration(), log_file_header()->node()->name()->string_view());
+ return configuration::GetNodeOrDie(configuration(),
+ log_file_header()->node());
} else {
CHECK(!log_file_header()->has_node());
return nullptr;
}
}
- // Pops a pointer to the channel with the oldest message in it, and the
- // timestamp.
- std::tuple<monotonic_clock::time_point, int, FlatbufferVector<MessageHeader>>
- PopOldestChannel();
+ // Returns the timestamp of the newest message read from the log file, and the
+ // timestamp that we need to re-queue data.
+ monotonic_clock::time_point newest_timestamp() const {
+ return newest_timestamp_;
+ }
+
+ // Returns the next time to trigger a requeue.
+ monotonic_clock::time_point time_to_queue() const { return time_to_queue_; }
+
+ // Returns the minimum amount of data needed to queue up for sorting before
+ // ware guarenteed to not see data out of order.
+ std::chrono::nanoseconds max_out_of_order_duration() const {
+ return message_reader_->max_out_of_order_duration();
+ }
+
+ std::string_view filename() const { return message_reader_->filename(); }
+
+ // Adds more messages to the sorted list. This reads enough data such that
+ // oldest_message_time can be replayed safely. Returns false if the log file
+ // has all been read.
+ bool QueueMessages(monotonic_clock::time_point oldest_message_time);
+
+ // Returns debug strings for a channel, and timestamps for a node.
+ std::string DebugString(int channel) const;
+ std::string DebugString(int channel, int node_index) const;
private:
+ // TODO(austin): Need to copy or refcount the message instead of running
+ // multiple copies of the reader. Or maybe have a "as_node" index and hide it
+ // inside.
+
// Moves to the next log file in the list.
bool NextLogFile();
- // Adds more messages to the sorted list.
- void QueueMessages();
+ // Filenames of the log files.
+ std::vector<std::string> filenames_;
+ // And the index of the next file to open.
+ size_t next_filename_index_ = 0;
- // Moves the message to the correct channel queue.
- void EmplaceDataBack(FlatbufferVector<MessageHeader> &&new_data);
-
- // Pushes a pointer to the channel for the given timestamp to the sorted
- // channel list.
- void PushChannelHeap(monotonic_clock::time_point timestamp,
- int channel_index);
-
+ // Log file header to report. This is a copy.
+ FlatbufferDetachedBuffer<LogFileHeader> log_file_header_;
+ // Current log file being read.
+ std::unique_ptr<MessageReader> message_reader_;
// Datastructure to hold the list of messages, cached timestamp for the
// oldest message, and sender to send with.
- struct ChannelData {
- monotonic_clock::time_point oldest_timestamp = monotonic_clock::min_time;
- std::deque<FlatbufferVector<MessageHeader>> data;
- std::unique_ptr<RawSender> raw_sender;
+ struct MessageHeaderQueue {
+ // If true, this is a timestamp queue.
+ bool timestamps = false;
- // Returns the oldest message.
- const FlatbufferVector<MessageHeader> &front() { return data.front(); }
-
- // Returns the timestamp for the oldest message.
- const monotonic_clock::time_point front_timestamp() {
- return monotonic_clock::time_point(
- std::chrono::nanoseconds(front().message().monotonic_sent_time()));
+ // Returns a reference to the the oldest message.
+ FlatbufferVector<MessageHeader> &front() {
+ CHECK_GT(data_.size(), 0u);
+ return data_.front();
}
+
+ // Adds a message to the back of the queue. Returns true if it was actually
+ // emplaced.
+ bool emplace_back(FlatbufferVector<MessageHeader> &&msg);
+
+ // Drops the front message. Invalidates the front() reference.
+ void pop_front();
+
+ // The size of the queue.
+ size_t size() { return data_.size(); }
+
+ // Returns a debug string with info about each message in the queue.
+ std::string DebugString() const;
+
+ // Returns the (timestamp, queue_index) for the oldest message.
+ const std::tuple<monotonic_clock::time_point, uint32_t,
+ const MessageHeader *>
+ front_timestamp() {
+ CHECK_GT(data_.size(), 0u);
+ return std::make_tuple(
+ monotonic_clock::time_point(std::chrono::nanoseconds(
+ front().message().monotonic_sent_time())),
+ front().message().queue_index(), &front().message());
+ }
+
+ // Pointer to the timestamp merger for this queue if available.
+ TimestampMerger *timestamp_merger = nullptr;
+ // Pointer to the reader which feeds this queue.
+ SplitMessageReader *split_reader = nullptr;
+
+ private:
+ // The data.
+ std::deque<FlatbufferVector<MessageHeader>> data_;
};
- std::vector<std::string> filenames_;
- size_t next_filename_index_ = 0;
+ // All the queues needed for a channel. There isn't going to be data in all
+ // of these.
+ struct ChannelData {
+ // The data queue for the channel.
+ MessageHeaderQueue data;
+ // Queues for timestamps for each node.
+ std::vector<MessageHeaderQueue> timestamps;
+ };
- FlatbufferDetachedBuffer<LogFileHeader> log_file_header_;
- std::unique_ptr<MessageReader> message_reader_;
-
- // TODO(austin): Multithreaded read at some point. Gotta go faster!
- // Especially if we start compressing.
-
- // List of channels and messages for them.
+ // Data for all the channels.
std::vector<ChannelData> channels_;
- // Heap of channels so we can track which channel to send next.
+ // Once we know the node that this SplitMessageReader will be writing as,
+ // there will be only one MessageHeaderQueue that a specific channel matches.
+ // Precompute this here for efficiency.
+ std::vector<MessageHeaderQueue *> channels_to_write_;
+
+ monotonic_clock::time_point time_to_queue_ = monotonic_clock::min_time;
+
+ // Latches true when we hit the end of the last log file and there is no sense
+ // poking it further.
+ bool at_end_ = false;
+
+ // Timestamp of the newest message that was read and actually queued. We want
+ // to track this independently from the log file because we need the
+ // timestamps here to be timestamps of messages that are queued.
+ monotonic_clock::time_point newest_timestamp_ = monotonic_clock::min_time;
+};
+
+class ChannelMerger;
+
+// Sorts channels (and timestamps) from multiple log files for a single channel.
+class TimestampMerger {
+ public:
+ TimestampMerger(const Configuration *configuration,
+ std::vector<SplitMessageReader *> split_message_readers,
+ int channel_index, const Node *target_node,
+ ChannelMerger *channel_merger);
+
+ // Metadata used to schedule the message.
+ struct DeliveryTimestamp {
+ monotonic_clock::time_point monotonic_event_time =
+ monotonic_clock::min_time;
+ realtime_clock::time_point realtime_event_time = realtime_clock::min_time;
+
+ monotonic_clock::time_point monotonic_remote_time =
+ monotonic_clock::min_time;
+ realtime_clock::time_point realtime_remote_time = realtime_clock::min_time;
+ uint32_t remote_queue_index = 0xffffffff;
+ };
+
+ // Pushes SplitMessageReader onto the timestamp heap. This should only be
+ // called when timestamps are placed in the channel this class is merging for
+ // the reader.
+ void UpdateTimestamp(
+ SplitMessageReader *split_message_reader,
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_message_time) {
+ PushTimestampHeap(oldest_message_time, split_message_reader);
+ }
+ // Pushes SplitMessageReader onto the message heap. This should only be
+ // called when data is placed in the channel this class is merging for the
+ // reader.
+ void Update(
+ SplitMessageReader *split_message_reader,
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_message_time) {
+ PushMessageHeap(oldest_message_time, split_message_reader);
+ }
+
+ // Returns the oldest combined timestamp and data for this channel. If there
+ // isn't a matching piece of data, returns only the timestamp with no data.
+ // The caller can determine what the appropriate action is to recover.
+ std::tuple<DeliveryTimestamp, FlatbufferVector<MessageHeader>> PopOldest();
+
+ // Tracks if the channel merger has pushed this onto it's heap or not.
+ bool pushed() { return pushed_; }
+ // Sets if this has been pushed to the channel merger heap. Should only be
+ // called by the channel merger.
+ void set_pushed(bool pushed) { pushed_ = pushed; }
+
+ // Returns a debug string with the heaps printed out.
+ std::string DebugString() const;
+
+ private:
+ // Pushes messages and timestamps to the corresponding heaps.
+ void PushMessageHeap(
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ timestamp,
+ SplitMessageReader *split_message_reader);
+ void PushTimestampHeap(
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ timestamp,
+ SplitMessageReader *split_message_reader);
+
+ // Pops a message from the message heap. This automatically triggers the
+ // split message reader to re-fetch any new data.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ PopMessageHeap();
+
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_message() const;
+ std::tuple<monotonic_clock::time_point, uint32_t, const MessageHeader *>
+ oldest_timestamp() const;
+ // Pops a message from the timestamp heap. This automatically triggers the
+ // split message reader to re-fetch any new data.
+ std::tuple<monotonic_clock::time_point, uint32_t,
+ FlatbufferVector<MessageHeader>>
+ PopTimestampHeap();
+
+ const Configuration *configuration_;
+
+ // If true, this is a forwarded channel and timestamps should be matched.
+ bool has_timestamps_ = false;
+
+ // Tracks if the ChannelMerger has pushed this onto it's queue.
+ bool pushed_ = false;
+
+ // The split message readers used for source data.
+ std::vector<SplitMessageReader *> split_message_readers_;
+
+ // The channel to merge.
+ int channel_index_;
+
+ // Our node.
+ int node_index_;
+
+ // Heaps for messages and timestamps.
+ std::vector<
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>>
+ message_heap_;
+ std::vector<
+ std::tuple<monotonic_clock::time_point, uint32_t, SplitMessageReader *>>
+ timestamp_heap_;
+
+ // Parent channel merger.
+ ChannelMerger *channel_merger_;
+};
+
+// This class handles constructing all the split message readers, channel
+// mergers, and combining the results.
+class ChannelMerger {
+ public:
+ // Builds a ChannelMerger around a set of log files. These are of the format:
+ // {
+ // {log1_part0, log1_part1, ...},
+ // {log2}
+ // }
+ // The inner vector is a list of log file chunks which form up a log file.
+ // The outer vector is a list of log files with subsets of the messages, or
+ // messages from different nodes.
+ ChannelMerger(const std::vector<std::vector<std::string>> &filenames);
+
+ // Returns the nodes that we know how to merge.
+ const std::vector<const Node *> nodes() const;
+ // Sets the node that we will return messages as. Returns true if the node
+ // has log files and will produce data. This can only be called once, and
+ // will likely corrupt state if called a second time.
+ bool SetNode(const Node *target_node);
+
+ // Everything else needs the node set before it works.
+
+ // Returns a timestamp for the oldest message in this group of logfiles.
+ monotonic_clock::time_point OldestMessage() const;
+ // Pops the oldest message.
+ std::tuple<TimestampMerger::DeliveryTimestamp, int,
+ FlatbufferVector<MessageHeader>>
+ PopOldest();
+
+ // Returns the config for this set of log files.
+ const Configuration *configuration() const {
+ return log_file_header()->configuration();
+ }
+
+ const LogFileHeader *log_file_header() const {
+ return &log_file_header_.message();
+ }
+
+ // Returns the start times for the configured node's log files.
+ monotonic_clock::time_point monotonic_start_time() const {
+ return monotonic_clock::time_point(
+ std::chrono::nanoseconds(log_file_header()->monotonic_start_time()));
+ }
+ realtime_clock::time_point realtime_start_time() const {
+ return realtime_clock::time_point(
+ std::chrono::nanoseconds(log_file_header()->realtime_start_time()));
+ }
+
+ // Returns the node set by SetNode above.
+ const Node *node() const { return node_; }
+
+ // Called by the TimestampMerger when new data is available with the provided
+ // timestamp and channel_index.
+ void Update(monotonic_clock::time_point timestamp, int channel_index) {
+ PushChannelHeap(timestamp, channel_index);
+ }
+
+ // Returns a debug string with all the heaps in it. Generally only useful for
+ // debugging what went wrong.
+ std::string DebugString() const;
+
+ private:
+ // Pushes the timestamp for new data on the provided channel.
+ void PushChannelHeap(monotonic_clock::time_point timestamp,
+ int channel_index);
+
+ // All the message readers.
+ std::vector<std::unique_ptr<SplitMessageReader>> split_message_readers_;
+
+ // The log header we are claiming to be.
+ FlatbufferDetachedBuffer<LogFileHeader> log_file_header_;
+
+ // The timestamp mergers which combine data from the split message readers.
+ std::vector<TimestampMerger> timestamp_mergers_;
+
+ // A heap of the channel readers and timestamps for the oldest data in each.
std::vector<std::pair<monotonic_clock::time_point, int>> channel_heap_;
+ // Configured node.
+ const Node *node_;
+
+ // Cached copy of the list of nodes.
+ std::vector<const Node *> nodes_;
};
} // namespace logger
diff --git a/aos/events/logging/logger.cc b/aos/events/logging/logger.cc
index c501d7b..c7f64ae 100644
--- a/aos/events/logging/logger.cc
+++ b/aos/events/logging/logger.cc
@@ -26,12 +26,22 @@
Logger::Logger(DetachedBufferWriter *writer, EventLoop *event_loop,
std::chrono::milliseconds polling_period)
+ : Logger(std::make_unique<LocalLogNamer>(writer, event_loop->node()),
+ event_loop, polling_period) {}
+
+Logger::Logger(std::unique_ptr<LogNamer> log_namer, EventLoop *event_loop,
+ std::chrono::milliseconds polling_period)
: event_loop_(event_loop),
- writer_(writer),
+ log_namer_(std::move(log_namer)),
timer_handler_(event_loop_->AddTimer([this]() { DoLogData(); })),
polling_period_(polling_period) {
+ VLOG(1) << "Starting logger for " << FlatbufferToJson(event_loop_->node());
+ int channel_index = 0;
for (const Channel *channel : *event_loop_->configuration()->channels()) {
FetcherStruct fs;
+ const bool is_local =
+ configuration::ChannelIsSendableOnNode(channel, event_loop_->node());
+
const bool is_readable =
configuration::ChannelIsReadableOnNode(channel, event_loop_->node());
const bool log_message = configuration::ChannelMessageIsLoggedOnNode(
@@ -50,28 +60,21 @@
<< configuration::CleanedChannelToString(channel);
if (log_delivery_times) {
- if (log_message) {
- VLOG(1) << " Logging message and delivery times";
- fs.log_type = LogType::kLogMessageAndDeliveryTime;
- } else {
- VLOG(1) << " Logging delivery times only";
- fs.log_type = LogType::kLogDeliveryTimeOnly;
- }
- } else {
- // We don't have a particularly great use case right now for logging a
- // forwarded message, but either not logging the delivery times, or
- // logging them on another node. Fail rather than produce bad results.
- CHECK(configuration::ChannelIsSendableOnNode(channel,
- event_loop_->node()))
- << ": Logger only knows how to log remote messages with "
- "forwarding timestamps.";
- VLOG(1) << " Logging message only";
- fs.log_type = LogType::kLogMessage;
+ VLOG(1) << " Delivery times";
+ fs.timestamp_writer = log_namer_->MakeTimestampWriter(channel);
}
+ if (log_message) {
+ VLOG(1) << " Data";
+ fs.writer = log_namer_->MakeWriter(channel);
+ if (!is_local) {
+ fs.log_type = LogType::kLogRemoteMessage;
+ }
+ }
+ fs.channel_index = channel_index;
+ fs.written = false;
+ fetchers_.emplace_back(std::move(fs));
}
-
- fs.written = false;
- fetchers_.emplace_back(std::move(fs));
+ ++channel_index;
}
// When things start, we want to log the header, then the most recent messages
@@ -82,9 +85,7 @@
// so we can capture the latest message on each channel. This lets us have
// non periodic messages with configuration that now get logged.
for (FetcherStruct &f : fetchers_) {
- if (f.fetcher.get() != nullptr) {
- f.written = !f.fetcher->Fetch();
- }
+ f.written = !f.fetcher->Fetch();
}
// We need to pick a point in time to declare the log file "started". This
@@ -95,7 +96,8 @@
realtime_start_time_ = event_loop_->realtime_now();
last_synchronized_time_ = monotonic_start_time_;
- LOG(INFO) << "Logging node as " << FlatbufferToJson(event_loop_->node());
+ LOG(INFO) << "Logging node as " << FlatbufferToJson(event_loop_->node())
+ << " start_time " << monotonic_start_time_;
WriteHeader();
@@ -104,7 +106,16 @@
});
}
+// TODO(austin): Set the remote start time to the first time we see a remote
+// message when we are logging those messages separate? Need to signal what to
+// do, or how to get a good timestamp.
+
void Logger::WriteHeader() {
+ for (const Node *node : log_namer_->nodes()) {
+ WriteHeader(node);
+ }
+}
+void Logger::WriteHeader(const Node *node) {
// Now write the header with this timestamp in it.
flatbuffers::FlatBufferBuilder fbb;
fbb.ForceDefaults(1);
@@ -117,7 +128,7 @@
flatbuffers::Offset<Node> node_offset;
if (event_loop_->node() != nullptr) {
- node_offset = CopyFlatBuffer(event_loop_->node(), &fbb);
+ node_offset = CopyFlatBuffer(node, &fbb);
}
aos::logger::LogFileHeader::Builder log_file_header_builder(fbb);
@@ -125,7 +136,7 @@
log_file_header_builder.add_name(string_offset);
// Only add the node if we are running in a multinode configuration.
- if (event_loop_->node() != nullptr) {
+ if (node != nullptr) {
log_file_header_builder.add_node(node_offset);
}
@@ -149,15 +160,32 @@
.count());
fbb.FinishSizePrefixed(log_file_header_builder.Finish());
- writer_->QueueSizedFlatbuffer(&fbb);
+ log_namer_->WriteHeader(&fbb, node);
}
void Logger::Rotate(DetachedBufferWriter *writer) {
+ Rotate(std::make_unique<LocalLogNamer>(writer, event_loop_->node()));
+}
+
+void Logger::Rotate(std::unique_ptr<LogNamer> log_namer) {
// Force data up until now to be written.
DoLogData();
// Swap the writer out, and re-write the header.
- writer_ = writer;
+ log_namer_ = std::move(log_namer);
+
+ // And then update the writers.
+ for (FetcherStruct &f : fetchers_) {
+ const Channel *channel =
+ event_loop_->configuration()->channels()->Get(f.channel_index);
+ if (f.timestamp_writer != nullptr) {
+ f.timestamp_writer = log_namer_->MakeTimestampWriter(channel);
+ }
+ if (f.writer != nullptr) {
+ f.writer = log_namer_->MakeWriter(channel);
+ }
+ }
+
WriteHeader();
}
@@ -173,61 +201,79 @@
// per iteration, even if it is small.
last_synchronized_time_ =
std::min(last_synchronized_time_ + polling_period_, monotonic_now);
- size_t channel_index = 0;
// Write each channel to disk, one at a time.
for (FetcherStruct &f : fetchers_) {
- // Skip any channels which we aren't supposed to log.
- if (f.fetcher.get() != nullptr) {
- while (true) {
- if (f.written) {
- if (!f.fetcher->FetchNext()) {
- VLOG(2) << "No new data on "
- << configuration::CleanedChannelToString(
- f.fetcher->channel());
- break;
- } else {
- f.written = false;
- }
+ while (true) {
+ if (f.written) {
+ if (!f.fetcher->FetchNext()) {
+ VLOG(2) << "No new data on "
+ << configuration::CleanedChannelToString(
+ f.fetcher->channel());
+ break;
+ } else {
+ f.written = false;
}
+ }
- CHECK(!f.written);
+ CHECK(!f.written);
- // TODO(james): Write tests to exercise this logic.
- if (f.fetcher->context().monotonic_event_time <
- last_synchronized_time_) {
+ // TODO(james): Write tests to exercise this logic.
+ if (f.fetcher->context().monotonic_event_time <
+ last_synchronized_time_) {
+ if (f.writer != nullptr) {
// Write!
flatbuffers::FlatBufferBuilder fbb(f.fetcher->context().size +
max_header_size_);
fbb.ForceDefaults(1);
fbb.FinishSizePrefixed(PackMessage(&fbb, f.fetcher->context(),
- channel_index, f.log_type));
+ f.channel_index, f.log_type));
- VLOG(2) << "Writing data for channel "
+ VLOG(2) << "Writing data as node "
+ << FlatbufferToJson(event_loop_->node()) << " for channel "
<< configuration::CleanedChannelToString(
- f.fetcher->channel());
+ f.fetcher->channel())
+ << " to " << f.writer->filename() << " data "
+ << FlatbufferToJson(
+ flatbuffers::GetSizePrefixedRoot<MessageHeader>(
+ fbb.GetBufferPointer()));
max_header_size_ = std::max(
max_header_size_, fbb.GetSize() - f.fetcher->context().size);
- writer_->QueueSizedFlatbuffer(&fbb);
-
- f.written = true;
- } else {
- break;
+ f.writer->QueueSizedFlatbuffer(&fbb);
}
+
+ if (f.timestamp_writer != nullptr) {
+ // And now handle timestamps.
+ flatbuffers::FlatBufferBuilder fbb;
+ fbb.ForceDefaults(1);
+
+ fbb.FinishSizePrefixed(PackMessage(&fbb, f.fetcher->context(),
+ f.channel_index,
+ LogType::kLogDeliveryTimeOnly));
+
+ VLOG(2) << "Writing timestamps as node "
+ << FlatbufferToJson(event_loop_->node()) << " for channel "
+ << configuration::CleanedChannelToString(
+ f.fetcher->channel())
+ << " to " << f.timestamp_writer->filename() << " timestamp "
+ << FlatbufferToJson(
+ flatbuffers::GetSizePrefixedRoot<MessageHeader>(
+ fbb.GetBufferPointer()));
+
+ f.timestamp_writer->QueueSizedFlatbuffer(&fbb);
+ }
+
+ f.written = true;
+ } else {
+ break;
}
}
-
- ++channel_index;
}
- CHECK_EQ(channel_index, fetchers_.size());
-
// If we missed cycles, we could be pretty far behind. Spin until we are
// caught up.
} while (last_synchronized_time_ + polling_period_ < monotonic_now);
-
- writer_->Flush();
}
LogReader::LogReader(std::string_view filename,
@@ -237,41 +283,58 @@
LogReader::LogReader(const std::vector<std::string> &filenames,
const Configuration *replay_configuration)
- : sorted_message_reader_(filenames),
+ : LogReader(std::vector<std::vector<std::string>>{filenames},
+ replay_configuration) {}
+
+LogReader::LogReader(const std::vector<std::vector<std::string>> &filenames,
+ const Configuration *replay_configuration)
+ : filenames_(filenames),
+ log_file_header_(ReadHeader(filenames[0][0])),
replay_configuration_(replay_configuration) {
- channels_.resize(logged_configuration()->channels()->size());
MakeRemappedConfig();
+
+ if (!configuration::MultiNode(configuration())) {
+ auto it = channel_mergers_.insert(std::make_pair(nullptr, State{}));
+ State *state = &(it.first->second);
+
+ state->channel_merger = std::make_unique<ChannelMerger>(filenames);
+ }
}
LogReader::~LogReader() { Deregister(); }
const Configuration *LogReader::logged_configuration() const {
- return sorted_message_reader_.configuration();
+ return log_file_header_.message().configuration();
}
const Configuration *LogReader::configuration() const {
return remapped_configuration_;
}
-const Node *LogReader::node() const {
+std::vector<const Node *> LogReader::Nodes() const {
// Because the Node pointer will only be valid if it actually points to memory
// owned by remapped_configuration_, we need to wait for the
// remapped_configuration_ to be populated before accessing it.
+ //
+ // Also, note, that when ever a map is changed, the nodes in here are
+ // invalidated.
CHECK(remapped_configuration_ != nullptr)
<< ": Need to call Register before the node() pointer will be valid.";
- if (sorted_message_reader_.node() == nullptr) {
- return nullptr;
- }
- return configuration::GetNode(
- configuration(), sorted_message_reader_.node()->name()->string_view());
+ return configuration::GetNodes(remapped_configuration_);
}
-monotonic_clock::time_point LogReader::monotonic_start_time() {
- return sorted_message_reader_.monotonic_start_time();
+monotonic_clock::time_point LogReader::monotonic_start_time(const Node *node) {
+ auto it = channel_mergers_.find(node);
+ CHECK(it != channel_mergers_.end())
+ << ": Unknown node " << FlatbufferToJson(node);
+ return it->second.channel_merger->monotonic_start_time();
}
-realtime_clock::time_point LogReader::realtime_start_time() {
- return sorted_message_reader_.realtime_start_time();
+realtime_clock::time_point LogReader::realtime_start_time(const Node *node) {
+ auto it = channel_mergers_.find(node);
+ CHECK(it != channel_mergers_.end())
+ << ": Unknown node " << FlatbufferToJson(node);
+ return it->second.channel_merger->realtime_start_time();
}
void LogReader::Register() {
@@ -282,126 +345,194 @@
void LogReader::Register(SimulatedEventLoopFactory *event_loop_factory) {
event_loop_factory_ = event_loop_factory;
- node_event_loop_factory_ =
- event_loop_factory_->GetNodeEventLoopFactory(node());
- event_loop_unique_ptr_ =
- event_loop_factory->MakeEventLoop("log_reader", node());
- // We don't run timing reports when trying to print out logged data, because
- // otherwise we would end up printing out the timing reports themselves...
- // This is only really relevant when we are replaying into a simulation.
- event_loop_unique_ptr_->SkipTimingReport();
- Register(event_loop_unique_ptr_.get());
- event_loop_factory_->RunFor(monotonic_start_time() -
- event_loop_->monotonic_now());
+ for (const Node *node : configuration::GetNodes(configuration())) {
+ auto it = channel_mergers_.insert(std::make_pair(node, State{}));
+
+ State *state = &(it.first->second);
+
+ state->channel_merger = std::make_unique<ChannelMerger>(filenames_);
+
+ state->node_event_loop_factory =
+ event_loop_factory_->GetNodeEventLoopFactory(node);
+ state->event_loop_unique_ptr =
+ event_loop_factory->MakeEventLoop("log_reader", node);
+
+ Register(state->event_loop_unique_ptr.get());
+ }
+
+ // Basic idea is that we want to
+ // 1) Find the node which booted first.
+ // 2) Setup the clocks so that each clock is at the time it would be at when
+ // the first node booted.
+
+ realtime_clock::time_point earliest_boot_time = realtime_clock::max_time;
+ for (std::pair<const Node *const, State> &state_pair : channel_mergers_) {
+ State *state = &(state_pair.second);
+
+ const realtime_clock::time_point boot_time =
+ state->channel_merger->realtime_start_time() -
+ state->channel_merger->monotonic_start_time().time_since_epoch();
+
+ if (boot_time < earliest_boot_time) {
+ earliest_boot_time = boot_time;
+ }
+ }
+
+ // We want to start the log file at the last start time of the log files from
+ // all the nodes. Compute how long each node's simulation needs to run to
+ // move time to this point.
+ monotonic_clock::duration run_time = monotonic_clock::duration(0);
+
+ for (std::pair<const Node *const, State> &state_pair : channel_mergers_) {
+ State *state = &(state_pair.second);
+
+ const realtime_clock::time_point boot_time =
+ state->channel_merger->realtime_start_time() -
+ state->channel_merger->monotonic_start_time().time_since_epoch();
+
+ // And start each node's clocks so the realtime clocks line up for the start
+ // times. This will let us start using it, but isn't good enough.
+ state->node_event_loop_factory->SetMonotonicNow(
+ monotonic_clock::time_point(earliest_boot_time - boot_time));
+ state->node_event_loop_factory->SetRealtimeOffset(
+ state->channel_merger->monotonic_start_time(),
+ state->channel_merger->realtime_start_time());
+ run_time =
+ std::max(run_time, state->channel_merger->monotonic_start_time() -
+ state->node_event_loop_factory->monotonic_now());
+ }
+
+ // Forwarding is tracked per channel. If it is enabled, we want to turn it
+ // off. Otherwise messages replayed will get forwarded across to the other
+ // nodes, and also replayed on the other nodes. This may not satisfy all our
+ // users, but it'll start the discussion.
+ if (configuration::MultiNode(event_loop_factory_->configuration())) {
+ for (size_t i = 0; i < logged_configuration()->channels()->size(); ++i) {
+ const Channel *channel = logged_configuration()->channels()->Get(i);
+ const Node *node = configuration::GetNode(
+ configuration(), channel->source_node()->string_view());
+
+ auto state_pair = channel_mergers_.find(node);
+ CHECK(state_pair != channel_mergers_.end());
+ State *state = &(state_pair->second);
+
+ const Channel *remapped_channel =
+ RemapChannel(state->event_loop, channel);
+
+ event_loop_factory_->DisableForwarding(remapped_channel);
+ }
+ }
+
+ // While we are starting the system up, we might be relying on matching data
+ // to timestamps on log files where the timestamp log file starts before the
+ // data. In this case, it is reasonable to expect missing data.
+ ignore_missing_data_ = true;
+ event_loop_factory_->RunFor(run_time);
+ // Now that we are running for real, missing data means that the log file is
+ // corrupted or went wrong.
+ ignore_missing_data_ = false;
}
void LogReader::Register(EventLoop *event_loop) {
- event_loop_ = event_loop;
+ auto state_pair = channel_mergers_.find(event_loop->node());
+ CHECK(state_pair != channel_mergers_.end());
+ State *state = &(state_pair->second);
+
+ state->event_loop = event_loop;
// We don't run timing reports when trying to print out logged data, because
// otherwise we would end up printing out the timing reports themselves...
// This is only really relevant when we are replaying into a simulation.
- // Otherwise we replay the timing report and try to resend it...
- event_loop_->SkipTimingReport();
- event_loop_->SkipAosLog();
+ event_loop->SkipTimingReport();
+ event_loop->SkipAosLog();
- for (size_t i = 0; i < channels_.size(); ++i) {
- const Channel *const original_channel =
- logged_configuration()->channels()->Get(i);
+ state->channel_merger->SetNode(event_loop->node());
- std::string_view channel_name = original_channel->name()->string_view();
- std::string_view channel_type = original_channel->type()->string_view();
- // If the channel is remapped, find the correct channel name to use.
- if (remapped_channels_.count(i) > 0) {
- VLOG(2) << "Got remapped channel on "
- << configuration::CleanedChannelToString(original_channel);
- channel_name = remapped_channels_[i];
- }
+ state->channels.resize(logged_configuration()->channels()->size());
- VLOG(1) << "Going to remap channel " << channel_name << " " << channel_type;
- const Channel *channel = configuration::GetChannel(
- event_loop_->configuration(), channel_name, channel_type,
- event_loop_->name(), event_loop_->node());
+ for (size_t i = 0; i < state->channels.size(); ++i) {
+ const Channel *channel =
+ RemapChannel(event_loop, logged_configuration()->channels()->Get(i));
- CHECK(channel != nullptr)
- << ": Unable to send {\"name\": \"" << channel_name
- << "\", \"type\": \"" << channel_type
- << "\"} because it is not in the provided configuration.";
-
- channels_[i] = event_loop_->MakeRawSender(channel);
+ state->channels[i] = event_loop->MakeRawSender(channel);
}
- timer_handler_ = event_loop_->AddTimer([this]() {
- if (sorted_message_reader_.active_channel_count() == 0u) {
- event_loop_factory_->Exit();
+ state->timer_handler = event_loop->AddTimer([this, state]() {
+ if (state->channel_merger->OldestMessage() == monotonic_clock::max_time) {
+ --live_nodes_;
+ if (live_nodes_ == 0) {
+ event_loop_factory_->Exit();
+ }
return;
}
- monotonic_clock::time_point channel_timestamp;
+ TimestampMerger::DeliveryTimestamp channel_timestamp;
int channel_index;
FlatbufferVector<MessageHeader> channel_data =
FlatbufferVector<MessageHeader>::Empty();
std::tie(channel_timestamp, channel_index, channel_data) =
- sorted_message_reader_.PopOldestChannel();
+ state->channel_merger->PopOldest();
const monotonic_clock::time_point monotonic_now =
- event_loop_->context().monotonic_event_time;
- CHECK(monotonic_now == channel_timestamp)
+ state->event_loop->context().monotonic_event_time;
+ CHECK(monotonic_now == channel_timestamp.monotonic_event_time)
<< ": Now " << monotonic_now.time_since_epoch().count()
- << " trying to send " << channel_timestamp.time_since_epoch().count();
+ << " trying to send "
+ << channel_timestamp.monotonic_event_time.time_since_epoch().count();
- if (channel_timestamp > monotonic_start_time() ||
+ if (channel_timestamp.monotonic_event_time >
+ state->channel_merger->monotonic_start_time() ||
event_loop_factory_ != nullptr) {
- if (!FLAGS_skip_missing_forwarding_entries ||
+ if ((!ignore_missing_data_ && !FLAGS_skip_missing_forwarding_entries) ||
channel_data.message().data() != nullptr) {
CHECK(channel_data.message().data() != nullptr)
<< ": Got a message without data. Forwarding entry which was "
- "not "
- "matched? Use --skip_missing_forwarding_entries to ignore "
+ "not matched? Use --skip_missing_forwarding_entries to ignore "
"this.";
// If we have access to the factory, use it to fix the realtime time.
- if (node_event_loop_factory_ != nullptr) {
- node_event_loop_factory_->SetRealtimeOffset(
- monotonic_clock::time_point(chrono::nanoseconds(
- channel_data.message().monotonic_sent_time())),
- realtime_clock::time_point(chrono::nanoseconds(
- channel_data.message().realtime_sent_time())));
+ if (state->node_event_loop_factory != nullptr) {
+ state->node_event_loop_factory->SetRealtimeOffset(
+ channel_timestamp.monotonic_event_time,
+ channel_timestamp.realtime_event_time);
}
- channels_[channel_index]->Send(
+ state->channels[channel_index]->Send(
channel_data.message().data()->Data(),
channel_data.message().data()->size(),
- monotonic_clock::time_point(chrono::nanoseconds(
- channel_data.message().monotonic_remote_time())),
- realtime_clock::time_point(chrono::nanoseconds(
- channel_data.message().realtime_remote_time())),
- channel_data.message().remote_queue_index());
+ channel_timestamp.monotonic_remote_time,
+ channel_timestamp.realtime_remote_time,
+ channel_timestamp.remote_queue_index);
}
} else {
- LOG(WARNING) << "Not sending data from before the start of the log file. "
- << channel_timestamp.time_since_epoch().count() << " start "
- << monotonic_start_time().time_since_epoch().count() << " "
- << FlatbufferToJson(channel_data);
+ LOG(WARNING)
+ << "Not sending data from before the start of the log file. "
+ << channel_timestamp.monotonic_event_time.time_since_epoch().count()
+ << " start " << monotonic_start_time().time_since_epoch().count()
+ << " " << FlatbufferToJson(channel_data);
}
- if (sorted_message_reader_.active_channel_count() > 0u) {
- timer_handler_->Setup(sorted_message_reader_.oldest_message().first);
+ const monotonic_clock::time_point next_time =
+ state->channel_merger->OldestMessage();
+ if (next_time != monotonic_clock::max_time) {
+ state->timer_handler->Setup(next_time);
} else {
// Set a timer up immediately after now to die. If we don't do this, then
// the senders waiting on the message we just read will never get called.
if (event_loop_factory_ != nullptr) {
- timer_handler_->Setup(monotonic_now +
- event_loop_factory_->send_delay() +
- std::chrono::nanoseconds(1));
+ state->timer_handler->Setup(monotonic_now +
+ event_loop_factory_->send_delay() +
+ std::chrono::nanoseconds(1));
}
}
});
- if (sorted_message_reader_.active_channel_count() > 0u) {
- event_loop_->OnRun([this]() {
- timer_handler_->Setup(sorted_message_reader_.oldest_message().first);
+ ++live_nodes_;
+
+ if (state->channel_merger->OldestMessage() != monotonic_clock::max_time) {
+ event_loop->OnRun([state]() {
+ state->timer_handler->Setup(state->channel_merger->OldestMessage());
});
}
}
@@ -409,15 +540,20 @@
void LogReader::Deregister() {
// Make sure that things get destroyed in the correct order, rather than
// relying on getting the order correct in the class definition.
- for (size_t i = 0; i < channels_.size(); ++i) {
- channels_[i].reset();
+ for (const Node *node : Nodes()) {
+ auto state_pair = channel_mergers_.find(node);
+ CHECK(state_pair != channel_mergers_.end());
+ State *state = &(state_pair->second);
+ for (size_t i = 0; i < state->channels.size(); ++i) {
+ state->channels[i].reset();
+ }
+ state->event_loop_unique_ptr.reset();
+ state->event_loop = nullptr;
+ state->node_event_loop_factory = nullptr;
}
- event_loop_unique_ptr_.reset();
- event_loop_ = nullptr;
event_loop_factory_unique_ptr_.reset();
event_loop_factory_ = nullptr;
- node_event_loop_factory_ = nullptr;
}
void LogReader::RemapLoggedChannel(std::string_view name, std::string_view type,
@@ -442,13 +578,15 @@
}
void LogReader::MakeRemappedConfig() {
- CHECK(!event_loop_)
- << ": Can't change the mapping after the events are scheduled.";
+ for (std::pair<const Node *const, State> &state : channel_mergers_) {
+ CHECK(!state.second.event_loop)
+ << ": Can't change the mapping after the events are scheduled.";
+ }
// If no remapping occurred and we are using the original config, then there
// is nothing interesting to do here.
if (remapped_channels_.empty() && replay_configuration_ == nullptr) {
- remapped_configuration_ = sorted_message_reader_.configuration();
+ remapped_configuration_ = logged_configuration();
return;
}
// Config to copy Channel definitions from. Use the specified
@@ -526,5 +664,30 @@
remapped_configuration_ = &remapped_configuration_buffer_->message();
}
+const Channel *LogReader::RemapChannel(const EventLoop *event_loop,
+ const Channel *channel) {
+ std::string_view channel_name = channel->name()->string_view();
+ std::string_view channel_type = channel->type()->string_view();
+ const int channel_index =
+ configuration::ChannelIndex(logged_configuration(), channel);
+ // If the channel is remapped, find the correct channel name to use.
+ if (remapped_channels_.count(channel_index) > 0) {
+ VLOG(2) << "Got remapped channel on "
+ << configuration::CleanedChannelToString(channel);
+ channel_name = remapped_channels_[channel_index];
+ }
+
+ VLOG(1) << "Going to remap channel " << channel_name << " " << channel_type;
+ const Channel *remapped_channel = configuration::GetChannel(
+ event_loop->configuration(), channel_name, channel_type,
+ event_loop->name(), event_loop->node());
+
+ CHECK(remapped_channel != nullptr)
+ << ": Unable to send {\"name\": \"" << channel_name << "\", \"type\": \""
+ << channel_type << "\"} because it is not in the provided configuration.";
+
+ return remapped_channel;
+}
+
} // namespace logger
} // namespace aos
diff --git a/aos/events/logging/logger.h b/aos/events/logging/logger.h
index 54b55d8..e0350bb 100644
--- a/aos/events/logging/logger.h
+++ b/aos/events/logging/logger.h
@@ -2,8 +2,8 @@
#define AOS_EVENTS_LOGGER_H_
#include <deque>
-#include <vector>
#include <string_view>
+#include <vector>
#include "absl/types/span.h"
#include "aos/events/event_loop.h"
@@ -16,6 +16,164 @@
namespace aos {
namespace logger {
+class LogNamer {
+ public:
+ LogNamer(const Node *node) : node_(node) { nodes_.emplace_back(node_); }
+ virtual ~LogNamer() {}
+
+ virtual void WriteHeader(flatbuffers::FlatBufferBuilder *fbb,
+ const Node *node) = 0;
+ virtual DetachedBufferWriter *MakeWriter(const Channel *channel) = 0;
+
+ virtual DetachedBufferWriter *MakeTimestampWriter(const Channel *channel) = 0;
+ const std::vector<const Node *> &nodes() const { return nodes_; }
+
+ const Node *node() const { return node_; }
+
+ protected:
+ const Node *const node_;
+ std::vector<const Node *> nodes_;
+};
+
+class LocalLogNamer : public LogNamer {
+ public:
+ LocalLogNamer(DetachedBufferWriter *writer, const Node *node)
+ : LogNamer(node), writer_(writer) {}
+
+ ~LocalLogNamer() override { writer_->Flush(); }
+
+ void WriteHeader(flatbuffers::FlatBufferBuilder *fbb,
+ const Node *node) override {
+ CHECK_EQ(node, this->node());
+ writer_->WriteSizedFlatbuffer(
+ absl::Span<const uint8_t>(fbb->GetBufferPointer(), fbb->GetSize()));
+ }
+
+ DetachedBufferWriter *MakeWriter(const Channel *channel) override {
+ CHECK(configuration::ChannelIsSendableOnNode(channel, node()));
+ return writer_;
+ }
+
+ DetachedBufferWriter *MakeTimestampWriter(const Channel *channel) override {
+ CHECK(configuration::ChannelIsReadableOnNode(channel, node_))
+ << ": Message is not delivered to this node.";
+ CHECK(node_ != nullptr) << ": Can't log timestamps in a single node world";
+ CHECK(configuration::ConnectionDeliveryTimeIsLoggedOnNode(channel, node_,
+ node_))
+ << ": Delivery times aren't logged for this channel on this node.";
+ return writer_;
+ }
+
+ private:
+ DetachedBufferWriter *writer_;
+};
+
+// TODO(austin): Split naming files from making files so we can re-use the
+// naming code to predict the log file names for a provided base name.
+class MultiNodeLogNamer : public LogNamer {
+ public:
+ MultiNodeLogNamer(std::string_view base_name,
+ const Configuration *configuration, const Node *node)
+ : LogNamer(node),
+ base_name_(base_name),
+ configuration_(configuration),
+ data_writer_(std::make_unique<DetachedBufferWriter>(absl::StrCat(
+ base_name_, "_", node->name()->string_view(), "_data.bfbs"))) {}
+
+ // Writes the header to all log files for a specific node. This function
+ // needs to be called after all the writers are created.
+ void WriteHeader(flatbuffers::FlatBufferBuilder *fbb, const Node *node) {
+ if (node == this->node()) {
+ data_writer_->WriteSizedFlatbuffer(
+ absl::Span<const uint8_t>(fbb->GetBufferPointer(), fbb->GetSize()));
+ } else {
+ for (std::pair<const Channel *const,
+ std::unique_ptr<DetachedBufferWriter>> &data_writer :
+ data_writers_) {
+ if (configuration::ChannelIsSendableOnNode(data_writer.first, node)) {
+ data_writer.second->WriteSizedFlatbuffer(absl::Span<const uint8_t>(
+ fbb->GetBufferPointer(), fbb->GetSize()));
+ }
+ }
+ }
+ }
+
+ // Makes a data logger for a specific channel.
+ DetachedBufferWriter *MakeWriter(const Channel *channel) {
+ // See if we can read the data on this node at all.
+ const bool is_readable =
+ configuration::ChannelIsReadableOnNode(channel, this->node());
+ if (!is_readable) {
+ return nullptr;
+ }
+
+ // Then, see if we are supposed to log the data here.
+ const bool log_message =
+ configuration::ChannelMessageIsLoggedOnNode(channel, this->node());
+
+ if (!log_message) {
+ return nullptr;
+ }
+
+ // Now, sort out if this is data generated on this node, or not. It is
+ // generated if it is sendable on this node.
+ if (configuration::ChannelIsSendableOnNode(channel, this->node())) {
+ return data_writer_.get();
+ } else {
+ // Ok, we have data that is being forwarded to us that we are supposed to
+ // log. It needs to be logged with send timestamps, but be sorted enough
+ // to be able to be processed.
+ CHECK(data_writers_.find(channel) == data_writers_.end());
+
+ // Track that this node is being logged.
+ if (configuration::MultiNode(configuration_)) {
+ const Node *source_node = configuration::GetNode(
+ configuration_, channel->source_node()->string_view());
+ if (std::find(nodes_.begin(), nodes_.end(), source_node) ==
+ nodes_.end()) {
+ nodes_.emplace_back(source_node);
+ }
+ }
+
+ return data_writers_
+ .insert(std::make_pair(
+ channel,
+ std::make_unique<DetachedBufferWriter>(absl::StrCat(
+ base_name_, "_", channel->source_node()->string_view(),
+ "_data", channel->name()->string_view(), "/",
+ channel->type()->string_view(), ".bfbs"))))
+ .first->second.get();
+ }
+ }
+
+ // Makes a timestamp (or timestamp and data) logger for a channel and
+ // forwarding connection.
+ DetachedBufferWriter *MakeTimestampWriter(const Channel *channel) {
+ const bool log_delivery_times =
+ (this->node() == nullptr)
+ ? false
+ : configuration::ConnectionDeliveryTimeIsLoggedOnNode(
+ channel, this->node(), this->node());
+ if (!log_delivery_times) {
+ return nullptr;
+ }
+
+ return data_writer_.get();
+ }
+
+ const std::vector<const Node *> &nodes() const { return nodes_; }
+
+ private:
+ const std::string base_name_;
+ const Configuration *const configuration_;
+
+ // File to write both delivery timestamps and local data to.
+ std::unique_ptr<DetachedBufferWriter> data_writer_;
+ // Files to write remote data to. We want one per channel.
+ std::map<const Channel *, std::unique_ptr<DetachedBufferWriter>>
+ data_writers_;
+};
+
// Logs all channels available in the event loop to disk every 100 ms.
// Start by logging one message per channel to capture any state and
// configuration that is sent rately on a channel and would affect execution.
@@ -24,18 +182,23 @@
Logger(DetachedBufferWriter *writer, EventLoop *event_loop,
std::chrono::milliseconds polling_period =
std::chrono::milliseconds(100));
+ Logger(std::unique_ptr<LogNamer> log_namer, EventLoop *event_loop,
+ std::chrono::milliseconds polling_period =
+ std::chrono::milliseconds(100));
// Rotates the log file with the new writer. This writes out the header
// again, but keeps going as if nothing else happened.
void Rotate(DetachedBufferWriter *writer);
+ void Rotate(std::unique_ptr<LogNamer> log_namer);
private:
void WriteHeader();
+ void WriteHeader(const Node *node);
void DoLogData();
EventLoop *event_loop_;
- DetachedBufferWriter *writer_;
+ std::unique_ptr<LogNamer> log_namer_;
// Structure to track both a fetcher, and if the data fetched has been
// written. We may want to delay writing data to disk so that we don't let
@@ -45,7 +208,12 @@
std::unique_ptr<RawFetcher> fetcher;
bool written = false;
- LogType log_type;
+ int channel_index = -1;
+
+ LogType log_type = LogType::kLogMessage;
+
+ DetachedBufferWriter *writer = nullptr;
+ DetachedBufferWriter *timestamp_writer = nullptr;
};
std::vector<FetcherStruct> fetchers_;
@@ -65,6 +233,34 @@
size_t max_header_size_ = 0;
};
+// We end up with one of the following 3 log file types.
+//
+// Single node logged as the source node.
+// -> Replayed just on the source node.
+//
+// Forwarding timestamps only logged from the perspective of the destination
+// node.
+// -> Matched with data on source node and logged.
+//
+// Forwarding timestamps with data logged as the destination node.
+// -> Replayed just as the destination
+// -> Replayed as the source (Much harder, ordering is not defined)
+//
+// Duplicate data logged. -> CHECK that it matches and explode otherwise.
+//
+// This can be boiled down to a set of constraints and tools.
+//
+// 1) Forwarding timestamps and data need to be logged separately.
+// 2) Any forwarded data logged on the destination node needs to be logged
+// separately such that it can be sorted.
+//
+// 1) Log reader needs to be able to sort a list of log files.
+// 2) Log reader needs to be able to merge sorted lists of log files.
+// 3) Log reader needs to be able to match timestamps with messages.
+//
+// We also need to be able to generate multiple views of a log file depending on
+// the target.
+
// Replays all the channels in the logfile to the event loop.
class LogReader {
public:
@@ -72,9 +268,23 @@
// (e.g., to change message rates, or to populate an updated schema), then
// pass it in here. It must provide all the channels that the original logged
// config did.
+ //
+ // Log filenames are in the following format:
+ //
+ // {
+ // {log1_part0, log1_part1, ...},
+ // {log2}
+ // }
+ // The inner vector is a list of log file chunks which form up a log file.
+ // The outer vector is a list of log files with subsets of the messages, or
+ // messages from different nodes.
+ //
+ // If the outer vector isn't provided, it is assumed to be of size 1.
LogReader(std::string_view filename,
const Configuration *replay_configuration = nullptr);
- LogReader(const std::vector<std::string> &filename,
+ LogReader(const std::vector<std::string> &filenames,
+ const Configuration *replay_configuration = nullptr);
+ LogReader(const std::vector<std::vector<std::string>> &filenames,
const Configuration *replay_configuration = nullptr);
~LogReader();
@@ -101,19 +311,17 @@
// Returns the configuration from the log file.
const Configuration *logged_configuration() const;
// Returns the configuration being used for replay.
+ // The pointer is invalidated whenever RemapLoggedChannel is called.
const Configuration *configuration() const;
- const LogFileHeader *log_file_header() const {
- return sorted_message_reader_.log_file_header();
- }
-
- // Returns the node that this log file was created on. This is a pointer to a
- // node in the nodes() list inside configuration().
- const Node *node() const;
+ // Returns the nodes that this log file was created on. This is a list of
+ // pointers to a node in the nodes() list inside configuration(). The
+ // pointers here are invalidated whenever RemapLoggedChannel is called.
+ std::vector<const Node *> Nodes() const;
// Returns the starting timestamp for the log file.
- monotonic_clock::time_point monotonic_start_time();
- realtime_clock::time_point realtime_start_time();
+ monotonic_clock::time_point monotonic_start_time(const Node *node = nullptr);
+ realtime_clock::time_point realtime_start_time(const Node *node = nullptr);
// Causes the logger to publish the provided channel on a different name so
// that replayed applications can publish on the proper channel name without
@@ -131,31 +339,48 @@
return event_loop_factory_;
}
- // TODO(austin): Add the ability to re-publish the fetched messages. Add 2
- // options, one which publishes them *now*, and another which publishes them
- // to the simulated event loop factory back in time where they actually
- // happened.
-
private:
+ const Channel *RemapChannel(const EventLoop *event_loop,
+ const Channel *channel);
+
+ const LogFileHeader *log_file_header() const {
+ return &log_file_header_.message();
+ }
+
// Queues at least max_out_of_order_duration_ messages into channels_.
void QueueMessages();
// Handle constructing a configuration with all the additional remapped
// channels from calls to RemapLoggedChannel.
void MakeRemappedConfig();
- // Log chunk reader.
- SortedMessageReader sorted_message_reader_;
+ const std::vector<std::vector<std::string>> filenames_;
+
+ // This is *a* log file header used to provide the logged config. The rest of
+ // the header is likely distracting.
+ FlatbufferVector<LogFileHeader> log_file_header_;
+
+ // State per node.
+ struct State {
+ // Log file.
+ std::unique_ptr<ChannelMerger> channel_merger;
+ // Senders.
+ std::vector<std::unique_ptr<RawSender>> channels;
+
+ // Factory (if we are in sim) that this loop was created on.
+ NodeEventLoopFactory *node_event_loop_factory = nullptr;
+ std::unique_ptr<EventLoop> event_loop_unique_ptr;
+ // Event loop.
+ EventLoop *event_loop = nullptr;
+ // And timer used to send messages.
+ TimerHandler *timer_handler;
+ };
+
+ // Map of nodes to States used to hold all the state for all the nodes.
+ std::map<const Node *, State> channel_mergers_;
std::unique_ptr<FlatbufferDetachedBuffer<Configuration>>
remapped_configuration_buffer_;
- std::vector<std::unique_ptr<RawSender>> channels_;
-
- std::unique_ptr<EventLoop> event_loop_unique_ptr_;
- NodeEventLoopFactory *node_event_loop_factory_ = nullptr;
- EventLoop *event_loop_ = nullptr;
- TimerHandler *timer_handler_;
-
std::unique_ptr<SimulatedEventLoopFactory> event_loop_factory_unique_ptr_;
SimulatedEventLoopFactory *event_loop_factory_ = nullptr;
@@ -164,8 +389,17 @@
// to send on instead of the logged channel name.
std::map<size_t, std::string> remapped_channels_;
+ // Number of nodes which still have data to send. This is used to figure out
+ // when to exit.
+ size_t live_nodes_ = 0;
+
const Configuration *remapped_configuration_ = nullptr;
const Configuration *replay_configuration_ = nullptr;
+
+ // If true, the replay timer will ignore any missing data. This is used
+ // during startup when we are bootstrapping everything and trying to get to
+ // the start of all the log files.
+ bool ignore_missing_data_ = false;
};
} // namespace logger
diff --git a/aos/events/logging/logger_main.cc b/aos/events/logging/logger_main.cc
index 74e9f65..5288e9b 100644
--- a/aos/events/logging/logger_main.cc
+++ b/aos/events/logging/logger_main.cc
@@ -22,8 +22,13 @@
aos::ShmEventLoop event_loop(&config.message());
- aos::logger::DetachedBufferWriter writer(aos::logging::GetLogName("fbs_log"));
- aos::logger::Logger logger(&writer, &event_loop);
+ std::unique_ptr<aos::logger::LogNamer> log_namer =
+ std::make_unique<aos::logger::MultiNodeLogNamer>(
+ aos::logging::GetLogName("fbs_log"), event_loop.configuration(),
+ event_loop.node());
+
+ aos::logger::Logger logger(std::move(log_namer), &event_loop,
+ std::chrono::milliseconds(100));
event_loop.Run();
diff --git a/aos/events/logging/logger_test.cc b/aos/events/logging/logger_test.cc
index 55d0ecc..9f2969e 100644
--- a/aos/events/logging/logger_test.cc
+++ b/aos/events/logging/logger_test.cc
@@ -5,6 +5,7 @@
#include "aos/events/pong_lib.h"
#include "aos/events/simulated_event_loop.h"
#include "glog/logging.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace aos {
@@ -70,7 +71,7 @@
// log file.
reader.Register();
- EXPECT_EQ(reader.node(), nullptr);
+ EXPECT_THAT(reader.Nodes(), ::testing::ElementsAre(nullptr));
std::unique_ptr<EventLoop> test_event_loop =
reader.event_loop_factory()->MakeEventLoop("log_reader");
@@ -135,7 +136,7 @@
// log file.
reader.Register();
- EXPECT_EQ(reader.node(), nullptr);
+ EXPECT_THAT(reader.Nodes(), ::testing::ElementsAre(nullptr));
std::unique_ptr<EventLoop> test_event_loop =
reader.event_loop_factory()->MakeEventLoop("log_reader");
@@ -209,77 +210,163 @@
: config_(aos::configuration::ReadConfig(
"aos/events/logging/multinode_pingpong_config.json")),
event_loop_factory_(&config_.message()),
- ping_event_loop_(event_loop_factory_.MakeEventLoop(
- "ping", configuration::GetNode(event_loop_factory_.configuration(),
- "pi1"))),
- ping_(ping_event_loop_.get()) {}
+ pi1_(
+ configuration::GetNode(event_loop_factory_.configuration(), "pi1")),
+ pi2_(configuration::GetNode(event_loop_factory_.configuration(),
+ "pi2")) {}
// Config and factory.
aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
SimulatedEventLoopFactory event_loop_factory_;
- // Event loop and app for Ping
- std::unique_ptr<EventLoop> ping_event_loop_;
- Ping ping_;
+ const Node *pi1_;
+ const Node *pi2_;
};
-// Tests that we can startup at all in a multinode configuration.
-TEST_F(MultinodeLoggerTest, MultiNode) {
- constexpr chrono::seconds kTimeOffset = chrono::seconds(10000);
- constexpr uint32_t kQueueIndexOffset = 1024;
- const ::std::string tmpdir(getenv("TEST_TMPDIR"));
- const ::std::string logfile = tmpdir + "/multi_logfile.bfbs";
- // Remove it.
- unlink(logfile.c_str());
+// Counts the number of messages on a channel (returns channel, count) for every
+// message matching matcher()
+std::vector<std::pair<int, int>> CountChannelsMatching(
+ std::string_view filename,
+ std::function<bool(const MessageHeader *)> matcher) {
+ MessageReader message_reader(filename);
+ std::vector<int> counts(
+ message_reader.log_file_header()->configuration()->channels()->size(), 0);
- LOG(INFO) << "Logging data to " << logfile;
+ while (true) {
+ std::optional<FlatbufferVector<MessageHeader>> msg =
+ message_reader.ReadMessage();
+ if (!msg) {
+ break;
+ }
+
+ if (matcher(&msg.value().message())) {
+ counts[msg.value().message().channel_index()]++;
+ }
+ }
+
+ std::vector<std::pair<int, int>> result;
+ int channel = 0;
+ for (size_t i = 0; i < counts.size(); ++i) {
+ if (counts[i] != 0) {
+ result.push_back(std::make_pair(channel, counts[i]));
+ }
+ ++channel;
+ }
+
+ return result;
+}
+
+// Counts the number of messages (channel, count) for all data messages.
+std::vector<std::pair<int, int>> CountChannelsData(std::string_view filename) {
+ return CountChannelsMatching(filename, [](const MessageHeader *msg) {
+ if (msg->has_data()) {
+ CHECK(!msg->has_monotonic_remote_time());
+ CHECK(!msg->has_realtime_remote_time());
+ CHECK(!msg->has_remote_queue_index());
+ return true;
+ }
+ return false;
+ });
+}
+
+// Counts the number of messages (channel, count) for all timestamp messages.
+std::vector<std::pair<int, int>> CountChannelsTimestamp(
+ std::string_view filename) {
+ return CountChannelsMatching(filename, [](const MessageHeader *msg) {
+ if (!msg->has_data()) {
+ CHECK(msg->has_monotonic_remote_time());
+ CHECK(msg->has_realtime_remote_time());
+ CHECK(msg->has_remote_queue_index());
+ return true;
+ }
+ return false;
+ });
+}
+
+// Tests that we can write and read simple multi-node log files.
+TEST_F(MultinodeLoggerTest, SimpleMultiNode) {
+ const ::std::string tmpdir(getenv("TEST_TMPDIR"));
+ const ::std::string logfile_base = tmpdir + "/multi_logfile";
+ const ::std::string logfile1 = logfile_base + "_pi1_data.bfbs";
+ const ::std::string logfile2 =
+ logfile_base + "_pi2_data/test/aos.examples.Pong.bfbs";
+ const ::std::string logfile3 = logfile_base + "_pi2_data.bfbs";
+
+ // Remove them.
+ unlink(logfile1.c_str());
+ unlink(logfile2.c_str());
+ unlink(logfile3.c_str());
+
+ LOG(INFO) << "Logging data to " << logfile1 << ", " << logfile2 << " and "
+ << logfile3;
{
- const Node *pi1 =
- configuration::GetNode(event_loop_factory_.configuration(), "pi1");
+ std::unique_ptr<EventLoop> ping_event_loop =
+ event_loop_factory_.MakeEventLoop("ping", pi1_);
+ Ping ping(ping_event_loop.get());
std::unique_ptr<EventLoop> pong_event_loop =
- event_loop_factory_.MakeEventLoop("pong", pi1);
+ event_loop_factory_.MakeEventLoop("pong", pi2_);
+ Pong pong(pong_event_loop.get());
- std::unique_ptr<aos::RawSender> pong_sender(
- pong_event_loop->MakeRawSender(aos::configuration::GetChannel(
- pong_event_loop->configuration(), "/test", "aos.examples.Pong",
- pong_event_loop->name(), pong_event_loop->node())));
+ std::unique_ptr<EventLoop> pi1_logger_event_loop =
+ event_loop_factory_.MakeEventLoop("logger", pi1_);
+ std::unique_ptr<LogNamer> pi1_log_namer =
+ std::make_unique<MultiNodeLogNamer>(
+ logfile_base, pi1_logger_event_loop->configuration(),
+ pi1_logger_event_loop->node());
- // Ok, let's fake a remote node. We use the fancy raw sender Send
- // method that message_gateway will use to do that.
- int pong_count = 0;
- pong_event_loop->MakeWatcher(
- "/test", [&pong_event_loop, &pong_count, &pong_sender,
- kTimeOffset](const examples::Ping &ping) {
- flatbuffers::FlatBufferBuilder fbb;
- examples::Pong::Builder pong_builder(fbb);
- pong_builder.add_value(ping.value());
- pong_builder.add_initial_send_time(ping.send_time());
- fbb.Finish(pong_builder.Finish());
-
- pong_sender->Send(fbb.GetBufferPointer(), fbb.GetSize(),
- pong_event_loop->monotonic_now() + kTimeOffset,
- pong_event_loop->realtime_now() + kTimeOffset,
- kQueueIndexOffset + pong_count);
- ++pong_count;
- });
-
- DetachedBufferWriter writer(logfile);
- std::unique_ptr<EventLoop> logger_event_loop =
- event_loop_factory_.MakeEventLoop("logger", pi1);
+ std::unique_ptr<EventLoop> pi2_logger_event_loop =
+ event_loop_factory_.MakeEventLoop("logger", pi2_);
+ std::unique_ptr<LogNamer> pi2_log_namer =
+ std::make_unique<MultiNodeLogNamer>(
+ logfile_base, pi2_logger_event_loop->configuration(),
+ pi2_logger_event_loop->node());
event_loop_factory_.RunFor(chrono::milliseconds(95));
- Logger logger(&writer, logger_event_loop.get(),
- std::chrono::milliseconds(100));
+ Logger pi1_logger(std::move(pi1_log_namer), pi1_logger_event_loop.get(),
+ std::chrono::milliseconds(100));
+
+ Logger pi2_logger(std::move(pi2_log_namer), pi2_logger_event_loop.get(),
+ std::chrono::milliseconds(100));
event_loop_factory_.RunFor(chrono::milliseconds(20000));
}
- LogReader reader(logfile);
+ {
+ // Confirm that the headers are all for the correct nodes.
+ FlatbufferVector<LogFileHeader> logheader1 = ReadHeader(logfile1);
+ EXPECT_EQ(logheader1.message().node()->name()->string_view(), "pi1");
+ FlatbufferVector<LogFileHeader> logheader2 = ReadHeader(logfile2);
+ EXPECT_EQ(logheader2.message().node()->name()->string_view(), "pi2");
+ FlatbufferVector<LogFileHeader> logheader3 = ReadHeader(logfile3);
+ EXPECT_EQ(logheader3.message().node()->name()->string_view(), "pi2");
- // TODO(austin): Also replay as pi2 or pi3 and make sure we see the pong
- // messages. This won't work today yet until the log reading code gets
- // significantly better.
+ // Timing reports, pings
+ EXPECT_THAT(CountChannelsData(logfile1),
+ ::testing::ElementsAre(::testing::Pair(1, 40),
+ ::testing::Pair(4, 2001)));
+ // Timestamps for pong
+ EXPECT_THAT(CountChannelsTimestamp(logfile1),
+ ::testing::ElementsAre(::testing::Pair(5, 2001)));
+
+ // Pong data.
+ EXPECT_THAT(CountChannelsData(logfile2),
+ ::testing::ElementsAre(::testing::Pair(5, 2001)));
+ // No timestamps
+ EXPECT_THAT(CountChannelsTimestamp(logfile2), ::testing::ElementsAre());
+
+ // Timing reports and pongs.
+ EXPECT_THAT(CountChannelsData(logfile3),
+ ::testing::ElementsAre(::testing::Pair(3, 40),
+ ::testing::Pair(5, 2001)));
+ // And ping timestamps.
+ EXPECT_THAT(CountChannelsTimestamp(logfile3),
+ ::testing::ElementsAre(::testing::Pair(4, 2001)));
+ }
+
+ LogReader reader(
+ {std::vector<std::string>{logfile1}, std::vector<std::string>{logfile3}});
+
SimulatedEventLoopFactory log_reader_factory(reader.logged_configuration());
log_reader_factory.set_send_delay(chrono::microseconds(0));
@@ -289,44 +376,419 @@
const Node *pi1 =
configuration::GetNode(log_reader_factory.configuration(), "pi1");
+ const Node *pi2 =
+ configuration::GetNode(log_reader_factory.configuration(), "pi2");
- ASSERT_NE(reader.node(), nullptr);
- EXPECT_EQ(reader.node()->name()->string_view(), "pi1");
+ EXPECT_THAT(reader.Nodes(), ::testing::ElementsAre(pi1, pi2));
reader.event_loop_factory()->set_send_delay(chrono::microseconds(0));
- std::unique_ptr<EventLoop> test_event_loop =
+ std::unique_ptr<EventLoop> pi1_event_loop =
log_reader_factory.MakeEventLoop("test", pi1);
+ std::unique_ptr<EventLoop> pi2_event_loop =
+ log_reader_factory.MakeEventLoop("test", pi2);
- int ping_count = 10;
- int pong_count = 10;
+ int pi1_ping_count = 10;
+ int pi2_ping_count = 10;
+ int pi1_pong_count = 10;
+ int pi2_pong_count = 10;
// Confirm that the ping value matches.
- test_event_loop->MakeWatcher("/test",
- [&ping_count](const examples::Ping &ping) {
- EXPECT_EQ(ping.value(), ping_count + 1);
- ++ping_count;
- });
- // Confirm that the ping and pong counts both match, and the value also
- // matches.
- test_event_loop->MakeWatcher(
- "/test", [&test_event_loop, &ping_count, &pong_count,
- kTimeOffset](const examples::Pong &pong) {
- EXPECT_EQ(test_event_loop->context().remote_queue_index,
- pong_count + kQueueIndexOffset);
- EXPECT_EQ(test_event_loop->context().monotonic_remote_time,
- test_event_loop->monotonic_now() + kTimeOffset);
- EXPECT_EQ(test_event_loop->context().realtime_remote_time,
- test_event_loop->realtime_now() + kTimeOffset);
+ pi1_event_loop->MakeWatcher(
+ "/test", [&pi1_ping_count, &pi1_event_loop](const examples::Ping &ping) {
+ VLOG(1) << "Pi1 ping " << FlatbufferToJson(&ping)
+ << pi1_event_loop->context().monotonic_remote_time << " -> "
+ << pi1_event_loop->context().monotonic_event_time;
+ EXPECT_EQ(ping.value(), pi1_ping_count + 1);
+ EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time,
+ pi1_ping_count * chrono::milliseconds(10) +
+ monotonic_clock::epoch());
+ EXPECT_EQ(pi1_event_loop->context().realtime_remote_time,
+ pi1_ping_count * chrono::milliseconds(10) +
+ realtime_clock::epoch());
+ EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time,
+ pi1_event_loop->context().monotonic_event_time);
+ EXPECT_EQ(pi1_event_loop->context().realtime_remote_time,
+ pi1_event_loop->context().realtime_event_time);
- EXPECT_EQ(pong.value(), pong_count + 1);
- ++pong_count;
- EXPECT_EQ(ping_count, pong_count);
+ ++pi1_ping_count;
+ });
+ pi2_event_loop->MakeWatcher(
+ "/test", [&pi2_ping_count, &pi2_event_loop](const examples::Ping &ping) {
+ VLOG(1) << "Pi2 ping " << FlatbufferToJson(&ping)
+ << pi2_event_loop->context().monotonic_remote_time << " -> "
+ << pi2_event_loop->context().monotonic_event_time;
+ EXPECT_EQ(ping.value(), pi2_ping_count + 1);
+
+ EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
+ pi2_ping_count * chrono::milliseconds(10) +
+ monotonic_clock::epoch());
+ EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
+ pi2_ping_count * chrono::milliseconds(10) +
+ realtime_clock::epoch());
+ EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time +
+ chrono::microseconds(150),
+ pi2_event_loop->context().monotonic_event_time);
+ EXPECT_EQ(pi2_event_loop->context().realtime_remote_time +
+ chrono::microseconds(150),
+ pi2_event_loop->context().realtime_event_time);
+ ++pi2_ping_count;
});
- log_reader_factory.RunFor(std::chrono::seconds(100));
- EXPECT_EQ(ping_count, 2010);
- EXPECT_EQ(pong_count, 2010);
+ constexpr ssize_t kQueueIndexOffset = 0;
+ // Confirm that the ping and pong counts both match, and the value also
+ // matches.
+ pi1_event_loop->MakeWatcher(
+ "/test", [&pi1_event_loop, &pi1_ping_count,
+ &pi1_pong_count](const examples::Pong &pong) {
+ VLOG(1) << "Pi1 pong " << FlatbufferToJson(&pong) << " at "
+ << pi1_event_loop->context().monotonic_remote_time << " -> "
+ << pi1_event_loop->context().monotonic_event_time;
+
+ EXPECT_EQ(pi1_event_loop->context().remote_queue_index,
+ pi1_pong_count + kQueueIndexOffset);
+ EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time,
+ chrono::microseconds(200) +
+ pi1_pong_count * chrono::milliseconds(10) +
+ monotonic_clock::epoch());
+ EXPECT_EQ(pi1_event_loop->context().realtime_remote_time,
+ chrono::microseconds(200) +
+ pi1_pong_count * chrono::milliseconds(10) +
+ realtime_clock::epoch());
+
+ EXPECT_EQ(pi1_event_loop->context().monotonic_remote_time +
+ chrono::microseconds(150),
+ pi1_event_loop->context().monotonic_event_time);
+ EXPECT_EQ(pi1_event_loop->context().realtime_remote_time +
+ chrono::microseconds(150),
+ pi1_event_loop->context().realtime_event_time);
+
+ EXPECT_EQ(pong.value(), pi1_pong_count + 1);
+ ++pi1_pong_count;
+ EXPECT_EQ(pi1_ping_count, pi1_pong_count);
+ });
+ pi2_event_loop->MakeWatcher(
+ "/test", [&pi2_event_loop, &pi2_ping_count,
+ &pi2_pong_count](const examples::Pong &pong) {
+ VLOG(1) << "Pi2 pong " << FlatbufferToJson(&pong) << " at "
+ << pi2_event_loop->context().monotonic_remote_time << " -> "
+ << pi2_event_loop->context().monotonic_event_time;
+
+ EXPECT_EQ(pi2_event_loop->context().remote_queue_index,
+ pi2_pong_count + kQueueIndexOffset - 9);
+
+ EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
+ chrono::microseconds(200) +
+ pi2_pong_count * chrono::milliseconds(10) +
+ monotonic_clock::epoch());
+ EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
+ chrono::microseconds(200) +
+ pi2_pong_count * chrono::milliseconds(10) +
+ realtime_clock::epoch());
+
+ EXPECT_EQ(pi2_event_loop->context().monotonic_remote_time,
+ pi2_event_loop->context().monotonic_event_time);
+ EXPECT_EQ(pi2_event_loop->context().realtime_remote_time,
+ pi2_event_loop->context().realtime_event_time);
+
+ EXPECT_EQ(pong.value(), pi2_pong_count + 1);
+ ++pi2_pong_count;
+ EXPECT_EQ(pi2_ping_count, pi2_pong_count);
+ });
+
+ log_reader_factory.Run();
+ EXPECT_EQ(pi1_ping_count, 2010);
+ EXPECT_EQ(pi2_ping_count, 2010);
+ EXPECT_EQ(pi1_pong_count, 2010);
+ EXPECT_EQ(pi2_pong_count, 2010);
+
+ reader.Deregister();
+}
+
+// Tests that we can read log files where they don't start at the same monotonic
+// time.
+TEST_F(MultinodeLoggerTest, StaggeredStart) {
+ const ::std::string tmpdir(getenv("TEST_TMPDIR"));
+ const ::std::string logfile_base = tmpdir + "/multi_logfile";
+ const ::std::string logfile1 = logfile_base + "_pi1_data.bfbs";
+ const ::std::string logfile2 =
+ logfile_base + "_pi2_data/test/aos.examples.Pong.bfbs";
+ const ::std::string logfile3 = logfile_base + "_pi2_data.bfbs";
+
+ // Remove them.
+ unlink(logfile1.c_str());
+ unlink(logfile2.c_str());
+ unlink(logfile3.c_str());
+
+ LOG(INFO) << "Logging data to " << logfile1 << " and " << logfile3;
+
+ {
+ std::unique_ptr<EventLoop> ping_event_loop =
+ event_loop_factory_.MakeEventLoop("ping", pi1_);
+ Ping ping(ping_event_loop.get());
+ std::unique_ptr<EventLoop> pong_event_loop =
+ event_loop_factory_.MakeEventLoop("pong", pi2_);
+ Pong pong(pong_event_loop.get());
+
+ std::unique_ptr<EventLoop> pi1_logger_event_loop =
+ event_loop_factory_.MakeEventLoop("logger", pi1_);
+ std::unique_ptr<LogNamer> pi1_log_namer =
+ std::make_unique<MultiNodeLogNamer>(
+ logfile_base, pi1_logger_event_loop->configuration(),
+ pi1_logger_event_loop->node());
+
+ std::unique_ptr<EventLoop> pi2_logger_event_loop =
+ event_loop_factory_.MakeEventLoop("logger", pi2_);
+ std::unique_ptr<LogNamer> pi2_log_namer =
+ std::make_unique<MultiNodeLogNamer>(
+ logfile_base, pi2_logger_event_loop->configuration(),
+ pi2_logger_event_loop->node());
+
+ event_loop_factory_.RunFor(chrono::milliseconds(95));
+
+ Logger pi1_logger(std::move(pi1_log_namer), pi1_logger_event_loop.get(),
+ std::chrono::milliseconds(100));
+
+ event_loop_factory_.RunFor(chrono::milliseconds(200));
+
+ Logger pi2_logger(std::move(pi2_log_namer), pi2_logger_event_loop.get(),
+ std::chrono::milliseconds(100));
+ event_loop_factory_.RunFor(chrono::milliseconds(20000));
+ }
+
+ LogReader reader(
+ {std::vector<std::string>{logfile1}, std::vector<std::string>{logfile3}});
+
+ SimulatedEventLoopFactory log_reader_factory(reader.logged_configuration());
+ log_reader_factory.set_send_delay(chrono::microseconds(0));
+
+ // This sends out the fetched messages and advances time to the start of the
+ // log file.
+ reader.Register(&log_reader_factory);
+
+ const Node *pi1 =
+ configuration::GetNode(log_reader_factory.configuration(), "pi1");
+ const Node *pi2 =
+ configuration::GetNode(log_reader_factory.configuration(), "pi2");
+
+ EXPECT_THAT(reader.Nodes(), ::testing::ElementsAre(pi1, pi2));
+
+ reader.event_loop_factory()->set_send_delay(chrono::microseconds(0));
+
+ std::unique_ptr<EventLoop> pi1_event_loop =
+ log_reader_factory.MakeEventLoop("test", pi1);
+ std::unique_ptr<EventLoop> pi2_event_loop =
+ log_reader_factory.MakeEventLoop("test", pi2);
+
+ int pi1_ping_count = 30;
+ int pi2_ping_count = 30;
+ int pi1_pong_count = 30;
+ int pi2_pong_count = 30;
+
+ // Confirm that the ping value matches.
+ pi1_event_loop->MakeWatcher(
+ "/test", [&pi1_ping_count, &pi1_event_loop](const examples::Ping &ping) {
+ VLOG(1) << "Pi1 ping " << FlatbufferToJson(&ping)
+ << pi1_event_loop->context().monotonic_remote_time << " -> "
+ << pi1_event_loop->context().monotonic_event_time;
+ EXPECT_EQ(ping.value(), pi1_ping_count + 1);
+
+ ++pi1_ping_count;
+ });
+ pi2_event_loop->MakeWatcher(
+ "/test", [&pi2_ping_count, &pi2_event_loop](const examples::Ping &ping) {
+ VLOG(1) << "Pi2 ping " << FlatbufferToJson(&ping)
+ << pi2_event_loop->context().monotonic_remote_time << " -> "
+ << pi2_event_loop->context().monotonic_event_time;
+ EXPECT_EQ(ping.value(), pi2_ping_count + 1);
+
+ ++pi2_ping_count;
+ });
+
+ // Confirm that the ping and pong counts both match, and the value also
+ // matches.
+ pi1_event_loop->MakeWatcher(
+ "/test", [&pi1_event_loop, &pi1_ping_count,
+ &pi1_pong_count](const examples::Pong &pong) {
+ VLOG(1) << "Pi1 pong " << FlatbufferToJson(&pong) << " at "
+ << pi1_event_loop->context().monotonic_remote_time << " -> "
+ << pi1_event_loop->context().monotonic_event_time;
+
+ EXPECT_EQ(pong.value(), pi1_pong_count + 1);
+ ++pi1_pong_count;
+ EXPECT_EQ(pi1_ping_count, pi1_pong_count);
+ });
+ pi2_event_loop->MakeWatcher(
+ "/test", [&pi2_event_loop, &pi2_ping_count,
+ &pi2_pong_count](const examples::Pong &pong) {
+ VLOG(1) << "Pi2 pong " << FlatbufferToJson(&pong) << " at "
+ << pi2_event_loop->context().monotonic_remote_time << " -> "
+ << pi2_event_loop->context().monotonic_event_time;
+
+ EXPECT_EQ(pong.value(), pi2_pong_count + 1);
+ ++pi2_pong_count;
+ EXPECT_EQ(pi2_ping_count, pi2_pong_count);
+ });
+
+ log_reader_factory.Run();
+ EXPECT_EQ(pi1_ping_count, 2030);
+ EXPECT_EQ(pi2_ping_count, 2030);
+ EXPECT_EQ(pi1_pong_count, 2030);
+ EXPECT_EQ(pi2_pong_count, 2030);
+
+ reader.Deregister();
+}
+// TODO(austin): We can write a test which recreates a logfile and confirms that
+// we get it back. That is the ultimate test.
+
+// Tests that we can read log files where the monotonic clocks don't match
+// correctly.
+TEST_F(MultinodeLoggerTest, MissmatchingTimeStart) {
+ const ::std::string tmpdir(getenv("TEST_TMPDIR"));
+ const ::std::string logfile_base = tmpdir + "/multi_logfile";
+ const ::std::string logfile1 = logfile_base + "_pi1_data.bfbs";
+ const ::std::string logfile2 =
+ logfile_base + "_pi2_data/test/aos.examples.Pong.bfbs";
+ const ::std::string logfile3 = logfile_base + "_pi2_data.bfbs";
+
+ // Remove them.
+ unlink(logfile1.c_str());
+ unlink(logfile2.c_str());
+ unlink(logfile3.c_str());
+
+ LOG(INFO) << "Logging data to " << logfile1 << " and " << logfile3;
+
+ {
+ NodeEventLoopFactory *pi2 = event_loop_factory_.GetNodeEventLoopFactory(pi2_);
+ LOG(INFO) << "pi2 times: " << pi2->monotonic_now() << " "
+ << pi2->realtime_now() << " distributed "
+ << pi2->ToDistributedClock(pi2->monotonic_now());
+
+ pi2->SetMonotonicNow(pi2->monotonic_now() + std::chrono::seconds(1000));
+ LOG(INFO) << "pi2 times: " << pi2->monotonic_now() << " "
+ << pi2->realtime_now() << " distributed "
+ << pi2->ToDistributedClock(pi2->monotonic_now());
+
+ std::unique_ptr<EventLoop> ping_event_loop =
+ event_loop_factory_.MakeEventLoop("ping", pi1_);
+ Ping ping(ping_event_loop.get());
+ std::unique_ptr<EventLoop> pong_event_loop =
+ event_loop_factory_.MakeEventLoop("pong", pi2_);
+ Pong pong(pong_event_loop.get());
+
+ std::unique_ptr<EventLoop> pi1_logger_event_loop =
+ event_loop_factory_.MakeEventLoop("logger", pi1_);
+ std::unique_ptr<LogNamer> pi1_log_namer =
+ std::make_unique<MultiNodeLogNamer>(
+ logfile_base, pi1_logger_event_loop->configuration(),
+ pi1_logger_event_loop->node());
+
+ std::unique_ptr<EventLoop> pi2_logger_event_loop =
+ event_loop_factory_.MakeEventLoop("logger", pi2_);
+ std::unique_ptr<LogNamer> pi2_log_namer =
+ std::make_unique<MultiNodeLogNamer>(
+ logfile_base, pi2_logger_event_loop->configuration(),
+ pi2_logger_event_loop->node());
+
+ event_loop_factory_.RunFor(chrono::milliseconds(95));
+
+ Logger pi1_logger(std::move(pi1_log_namer), pi1_logger_event_loop.get(),
+ std::chrono::milliseconds(100));
+
+ event_loop_factory_.RunFor(chrono::milliseconds(200));
+
+ Logger pi2_logger(std::move(pi2_log_namer), pi2_logger_event_loop.get(),
+ std::chrono::milliseconds(100));
+ event_loop_factory_.RunFor(chrono::milliseconds(20000));
+ }
+
+ LogReader reader(
+ {std::vector<std::string>{logfile1}, std::vector<std::string>{logfile3}});
+
+ SimulatedEventLoopFactory log_reader_factory(reader.logged_configuration());
+ log_reader_factory.set_send_delay(chrono::microseconds(0));
+
+ // This sends out the fetched messages and advances time to the start of the
+ // log file.
+ reader.Register(&log_reader_factory);
+
+
+ const Node *pi1 =
+ configuration::GetNode(log_reader_factory.configuration(), "pi1");
+ const Node *pi2 =
+ configuration::GetNode(log_reader_factory.configuration(), "pi2");
+
+ LOG(INFO) << "Done registering (pi1) "
+ << log_reader_factory.GetNodeEventLoopFactory(pi1)->monotonic_now() << " "
+ << log_reader_factory.GetNodeEventLoopFactory(pi1)->realtime_now();
+ LOG(INFO) << "Done registering (pi2) "
+ << log_reader_factory.GetNodeEventLoopFactory(pi2)->monotonic_now() << " "
+ << log_reader_factory.GetNodeEventLoopFactory(pi2)->realtime_now();
+
+ EXPECT_THAT(reader.Nodes(), ::testing::ElementsAre(pi1, pi2));
+
+ reader.event_loop_factory()->set_send_delay(chrono::microseconds(0));
+
+ std::unique_ptr<EventLoop> pi1_event_loop =
+ log_reader_factory.MakeEventLoop("test", pi1);
+ std::unique_ptr<EventLoop> pi2_event_loop =
+ log_reader_factory.MakeEventLoop("test", pi2);
+
+ int pi1_ping_count = 30;
+ int pi2_ping_count = 30;
+ int pi1_pong_count = 30;
+ int pi2_pong_count = 30;
+
+ // Confirm that the ping value matches.
+ pi1_event_loop->MakeWatcher(
+ "/test", [&pi1_ping_count, &pi1_event_loop](const examples::Ping &ping) {
+ VLOG(1) << "Pi1 ping " << FlatbufferToJson(&ping)
+ << pi1_event_loop->context().monotonic_remote_time << " -> "
+ << pi1_event_loop->context().monotonic_event_time;
+ EXPECT_EQ(ping.value(), pi1_ping_count + 1);
+
+ ++pi1_ping_count;
+ });
+ pi2_event_loop->MakeWatcher(
+ "/test", [&pi2_ping_count, &pi2_event_loop](const examples::Ping &ping) {
+ VLOG(1) << "Pi2 ping " << FlatbufferToJson(&ping)
+ << pi2_event_loop->context().monotonic_remote_time << " -> "
+ << pi2_event_loop->context().monotonic_event_time;
+ EXPECT_EQ(ping.value(), pi2_ping_count + 1);
+
+ ++pi2_ping_count;
+ });
+
+ // Confirm that the ping and pong counts both match, and the value also
+ // matches.
+ pi1_event_loop->MakeWatcher(
+ "/test", [&pi1_event_loop, &pi1_ping_count,
+ &pi1_pong_count](const examples::Pong &pong) {
+ VLOG(1) << "Pi1 pong " << FlatbufferToJson(&pong) << " at "
+ << pi1_event_loop->context().monotonic_remote_time << " -> "
+ << pi1_event_loop->context().monotonic_event_time;
+
+ EXPECT_EQ(pong.value(), pi1_pong_count + 1);
+ ++pi1_pong_count;
+ EXPECT_EQ(pi1_ping_count, pi1_pong_count);
+ });
+ pi2_event_loop->MakeWatcher(
+ "/test", [&pi2_event_loop, &pi2_ping_count,
+ &pi2_pong_count](const examples::Pong &pong) {
+ VLOG(1) << "Pi2 pong " << FlatbufferToJson(&pong) << " at "
+ << pi2_event_loop->context().monotonic_remote_time << " -> "
+ << pi2_event_loop->context().monotonic_event_time;
+
+ EXPECT_EQ(pong.value(), pi2_pong_count + 1);
+ ++pi2_pong_count;
+ EXPECT_EQ(pi2_ping_count, pi2_pong_count);
+ });
+
+ log_reader_factory.Run();
+ EXPECT_EQ(pi1_ping_count, 2030);
+ EXPECT_EQ(pi2_ping_count, 2030);
+ EXPECT_EQ(pi1_pong_count, 2030);
+ EXPECT_EQ(pi2_pong_count, 2030);
reader.Deregister();
}
diff --git a/aos/events/logging/multinode_pingpong.json b/aos/events/logging/multinode_pingpong.json
index be8a402..957cc2b 100644
--- a/aos/events/logging/multinode_pingpong.json
+++ b/aos/events/logging/multinode_pingpong.json
@@ -33,9 +33,7 @@
"num_senders": 20,
"max_size": 2048
},
- /* Forwarded to pi2.
- * Doesn't matter where timestamps are logged for the test.
- */
+ /* Forwarded to pi2 */
{
"name": "/test",
"type": "aos.examples.Ping",
@@ -44,7 +42,7 @@
{
"name": "pi2",
"priority": 1,
- "timestamp_logger": "REMOTE_LOGGER",
+ "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
"timestamp_logger_node": "pi1",
"time_to_live": 5000000
}
diff --git a/aos/events/simulated_event_loop.cc b/aos/events/simulated_event_loop.cc
index 01574fa..faa9fe9 100644
--- a/aos/events/simulated_event_loop.cc
+++ b/aos/events/simulated_event_loop.cc
@@ -755,15 +755,8 @@
SimulatedEventLoopFactory::SimulatedEventLoopFactory(
const Configuration *configuration)
- : configuration_(CHECK_NOTNULL(configuration)) {
- if (configuration::MultiNode(configuration_)) {
- for (const Node *node : *configuration->nodes()) {
- nodes_.emplace_back(node);
- }
- } else {
- nodes_.emplace_back(nullptr);
- }
-
+ : configuration_(CHECK_NOTNULL(configuration)),
+ nodes_(configuration::GetNodes(configuration_)) {
for (const Node *node : nodes_) {
node_factories_.emplace_back(
new NodeEventLoopFactory(&scheduler_, this, node, &raw_event_loops_));
@@ -838,4 +831,8 @@
}
}
+void SimulatedEventLoopFactory::DisableForwarding(const Channel *channel) {
+ bridge_->DisableForwarding(channel);
+}
+
} // namespace aos
diff --git a/aos/events/simulated_event_loop.h b/aos/events/simulated_event_loop.h
index 8cff0d7..9dd3d1f 100644
--- a/aos/events/simulated_event_loop.h
+++ b/aos/events/simulated_event_loop.h
@@ -98,6 +98,10 @@
// Returns the configuration used for everything.
const Configuration *configuration() const { return configuration_; }
+ // Disables forwarding for this channel. This should be used very rarely only
+ // for things like the logger.
+ void DisableForwarding(const Channel *channel);
+
private:
const Configuration *const configuration_;
EventScheduler scheduler_;
@@ -150,6 +154,14 @@
inline distributed_clock::time_point ToDistributedClock(
monotonic_clock::time_point time) const;
+ // Note: use this very very carefully. It can cause massive problems. This
+ // needs to go away as we properly handle time drifting between nodes.
+ void SetMonotonicNow(monotonic_clock::time_point monotonic_now) {
+ monotonic_clock::duration offset = (monotonic_now - this->monotonic_now());
+ monotonic_offset_ += offset;
+ realtime_offset_ -= offset;
+ }
+
private:
friend class SimulatedEventLoopFactory;
NodeEventLoopFactory(
diff --git a/aos/events/simulated_network_bridge.cc b/aos/events/simulated_network_bridge.cc
index 57f2efd..6b03b2f 100644
--- a/aos/events/simulated_network_bridge.cc
+++ b/aos/events/simulated_network_bridge.cc
@@ -28,6 +28,8 @@
Schedule();
}
+ const Channel *channel() const { return fetcher_->channel(); }
+
// Kicks us to re-fetch and schedule the timer.
void Schedule() {
if (fetcher_->context().data == nullptr || sent_) {
@@ -107,10 +109,14 @@
// Pre-build up event loops for every node. They are pretty cheap anyways.
for (const Node *node : simulated_event_loop_factory->nodes()) {
- CHECK(event_loop_map_
- .insert({node, simulated_event_loop_factory->MakeEventLoop(
- "message_bridge", node)})
- .second);
+ auto it = event_loop_map_.insert(
+ {node,
+ simulated_event_loop_factory->MakeEventLoop("message_bridge", node)});
+
+ CHECK(it.second);
+
+ it.first->second->SkipTimingReport();
+ it.first->second->SkipAosLog();
}
for (const Channel *channel :
@@ -161,5 +167,22 @@
SimulatedMessageBridge::~SimulatedMessageBridge() {}
+void SimulatedMessageBridge::DisableForwarding(const Channel *channel) {
+ for (std::unique_ptr<std::vector<std::unique_ptr<RawMessageDelayer>>>
+ &delayers : delayers_list_) {
+ if (delayers->size() > 0) {
+ if ((*delayers)[0]->channel() == channel) {
+ for (std::unique_ptr<RawMessageDelayer> &delayer : *delayers) {
+ CHECK(delayer->channel() == channel);
+ }
+
+ // If we clear the delayers list, nothing will be scheduled. Which is a
+ // success!
+ delayers->clear();
+ }
+ }
+ }
+}
+
} // namespace message_bridge
} // namespace aos
diff --git a/aos/events/simulated_network_bridge.h b/aos/events/simulated_network_bridge.h
index 5d613ab..7aeef64 100644
--- a/aos/events/simulated_network_bridge.h
+++ b/aos/events/simulated_network_bridge.h
@@ -19,12 +19,15 @@
SimulatedEventLoopFactory *simulated_event_loop_factory);
~SimulatedMessageBridge();
+ // Disables forwarding for this channel. This should be used very rarely only
+ // for things like the logger.
+ void DisableForwarding(const Channel *channel);
+
private:
// Map of nodes to event loops. This is a member variable so that the
// lifetime of the event loops matches the lifetime of the bridge.
std::map<const Node *, std::unique_ptr<aos::EventLoop>> event_loop_map_;
-
// List of delayers used to resend the messages.
using DelayersVector = std::vector<std::unique_ptr<RawMessageDelayer>>;
std::vector<std::unique_ptr<DelayersVector>> delayers_list_;
diff --git a/aos/flatbuffer_merge.cc b/aos/flatbuffer_merge.cc
index c8cc742..1f5c8a0 100644
--- a/aos/flatbuffer_merge.cc
+++ b/aos/flatbuffer_merge.cc
@@ -526,4 +526,27 @@
return fbb.Release();
}
+bool CompareFlatBuffer(const flatbuffers::TypeTable *typetable,
+ const flatbuffers::Table *t1,
+ const flatbuffers::Table *t2) {
+ // Copying flatbuffers is deterministic for the same typetable. So, copy both
+ // to guarantee that they are sorted the same, then check that the memory
+ // matches.
+ //
+ // There has to be a better way to do this, but the efficiency hit of this
+ // implementation is fine for the usages that we have now. We are better off
+ // abstracting this into a library call where we can fix it later easily.
+ flatbuffers::FlatBufferBuilder fbb1;
+ fbb1.ForceDefaults(1);
+ fbb1.Finish(MergeFlatBuffers(typetable, t1, nullptr, &fbb1));
+ flatbuffers::FlatBufferBuilder fbb2;
+ fbb2.ForceDefaults(1);
+ fbb2.Finish(MergeFlatBuffers(typetable, t2, nullptr, &fbb2));
+
+ if (fbb1.GetSize() != fbb2.GetSize()) return false;
+
+ return memcmp(fbb1.GetBufferPointer(), fbb2.GetBufferPointer(),
+ fbb1.GetSize()) == 0;
+}
+
} // namespace aos
diff --git a/aos/flatbuffer_merge.h b/aos/flatbuffer_merge.h
index dafba34..5e84160 100644
--- a/aos/flatbuffer_merge.h
+++ b/aos/flatbuffer_merge.h
@@ -77,6 +77,18 @@
return FlatbufferDetachedBuffer<T>(fbb.Release());
}
+// Compares 2 flatbuffers. Returns true if they match, false otherwise.
+bool CompareFlatBuffer(const flatbuffers::TypeTable *typetable,
+ const flatbuffers::Table *t1,
+ const flatbuffers::Table *t2);
+
+template <class T>
+inline bool CompareFlatBuffer(const T *t1, const T *t2) {
+ return CompareFlatBuffer(T::MiniReflectTypeTable(),
+ reinterpret_cast<const flatbuffers::Table *>(t1),
+ reinterpret_cast<const flatbuffers::Table *>(t2));
+}
+
} // namespace aos
#endif // AOS_FLATBUFFER_MERGE_H_
diff --git a/aos/flatbuffer_merge_test.cc b/aos/flatbuffer_merge_test.cc
index 079eb11..371639a 100644
--- a/aos/flatbuffer_merge_test.cc
+++ b/aos/flatbuffer_merge_test.cc
@@ -20,6 +20,13 @@
const ::std::string merged_output =
FlatbufferToJson(fb_merged, ConfigurationTypeTable());
EXPECT_EQ(expected_output, merged_output);
+
+ aos::FlatbufferDetachedBuffer<Configuration> expected_message(
+ JsonToFlatbuffer(std::string(expected_output).c_str(),
+ ConfigurationTypeTable()));
+ EXPECT_TRUE(
+ CompareFlatBuffer(flatbuffers::GetRoot<Configuration>(fb_merged.data()),
+ &expected_message.message()));
}
void JsonMerge(const ::std::string in1, const ::std::string in2,
@@ -335,6 +342,18 @@
"\"name\": \"woo2\" }, { \"name\": \"wo3\" } ] }");
}
+// Tests a compare of 2 basic (different) messages.
+TEST_F(FlatbufferMerge, CompareDifferent) {
+ aos::FlatbufferDetachedBuffer<Configuration> message1(JsonToFlatbuffer(
+ "{ \"single_application\": { \"name\": \"wow\", \"priority\": 7 } }",
+ ConfigurationTypeTable()));
+ aos::FlatbufferDetachedBuffer<Configuration> message2(JsonToFlatbuffer(
+ "{ \"single_application\": { \"name\": \"wow\", \"priority\": 8 } }",
+ ConfigurationTypeTable()));
+
+ EXPECT_FALSE(CompareFlatBuffer(&message1.message(), &message2.message()));
+}
+
// TODO(austin): enums
// TODO(austin): unions
// TODO(austin): struct
diff --git a/aos/flatbuffers.h b/aos/flatbuffers.h
index d9fcab6..e556c0f 100644
--- a/aos/flatbuffers.h
+++ b/aos/flatbuffers.h
@@ -4,6 +4,7 @@
#include <array>
#include <string_view>
+#include "absl/types/span.h"
#include "flatbuffers/flatbuffers.h"
#include "glog/logging.h"
@@ -106,6 +107,11 @@
virtual const uint8_t *data() const = 0;
virtual uint8_t *data() = 0;
virtual size_t size() const = 0;
+
+ absl::Span<uint8_t> span() { return absl::Span<uint8_t>(data(), size()); }
+ absl::Span<const uint8_t> span() const {
+ return absl::Span<const uint8_t>(data(), size());
+ }
};
// String backed flatbuffer.
diff --git a/aos/network/BUILD b/aos/network/BUILD
index 9d3b3c4..8f049e3 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -1,4 +1,5 @@
-load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+load("//aos/seasocks:gen_embedded.bzl", "gen_embedded")
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_ts_library")
load("//aos:config.bzl", "aos_config")
package(default_visibility = ["//visibility:public"])
@@ -235,3 +236,62 @@
"//aos/testing:googletest",
],
)
+
+flatbuffer_cc_library(
+ name = "web_proxy_fbs",
+ srcs = ["web_proxy.fbs"],
+ gen_reflections = True,
+)
+
+flatbuffer_ts_library(
+ name = "web_proxy_ts_fbs",
+ srcs = ["web_proxy.fbs"],
+)
+
+cc_library(
+ name = "web_proxy",
+ hdrs = ["web_proxy.h"],
+ srcs = ["web_proxy.cc"],
+ copts = [
+ "-DWEBRTC_POSIX",
+ "-Wno-unused-parameter",
+ ],
+ deps = [
+ ":web_proxy_fbs",
+ "//aos/seasocks:seasocks_logger",
+ "//third_party/seasocks",
+ "//third_party:webrtc",
+ "@com_github_google_glog//:glog",
+ ],
+)
+
+gen_embedded(
+ name = "gen_embedded",
+ srcs = glob(
+ include = ["www_defaults/**/*"],
+ exclude = ["www/**/*"],
+ ),
+)
+
+cc_binary(
+ name = "web_proxy_main",
+ srcs = ["web_proxy_main.cc"],
+ deps = [
+ ":web_proxy",
+ ":gen_embedded",
+ "//aos:init",
+ "//aos/seasocks:seasocks_logger",
+ "//third_party/seasocks",
+ "@com_github_google_flatbuffers//:flatbuffers"
+ ],
+ copts = [
+ "-DWEBRTC_POSIX",
+ "-Wno-unused-parameter",
+ ],
+ data = [
+ "//aos/network/www:files",
+ "//aos/network/www:proxy_bundle",
+ "//aos/network/www:flatbuffers",
+ "@com_github_google_flatbuffers//:flatjs"
+ ],
+)
diff --git a/aos/network/web_proxy.cc b/aos/network/web_proxy.cc
new file mode 100644
index 0000000..c6d7336
--- /dev/null
+++ b/aos/network/web_proxy.cc
@@ -0,0 +1,163 @@
+#include "aos/network/web_proxy.h"
+#include "aos/network/web_proxy_generated.h"
+#include "api/create_peerconnection_factory.h"
+#include "glog/logging.h"
+
+namespace aos {
+namespace web_proxy {
+
+namespace {
+// Based on webrtc examples. In our controlled environment we expect setting sdp
+// to always succeed, and we can't do anything about a failure, so just ignore
+// everything.
+class DummySetSessionDescriptionObserver
+ : public webrtc::SetSessionDescriptionObserver {
+ public:
+ static DummySetSessionDescriptionObserver *Create() {
+ return new rtc::RefCountedObject<DummySetSessionDescriptionObserver>();
+ }
+ virtual void OnSuccess() {}
+ virtual void OnFailure(webrtc::RTCError error) {}
+};
+
+} // namespace
+
+WebsocketHandler::WebsocketHandler(::seasocks::Server *server)
+ : server_(server) {}
+
+void WebsocketHandler::onConnect(::seasocks::WebSocket *sock) {
+ std::unique_ptr<Connection> conn =
+ std::make_unique<Connection>(sock, server_);
+ connections_.insert({sock, std::move(conn)});
+}
+
+void WebsocketHandler::onData(::seasocks::WebSocket *sock, const uint8_t *data,
+ size_t size) {
+ connections_[sock]->HandleWebSocketData(data, size);
+}
+
+void WebsocketHandler::onDisconnect(::seasocks::WebSocket *sock) {
+ connections_.erase(sock);
+}
+
+Connection::Connection(::seasocks::WebSocket *sock, ::seasocks::Server *server)
+ : sock_(sock), server_(server) {}
+
+// Function called for web socket data. Parses the flatbuffer and handles it
+// appropriately.
+void Connection::HandleWebSocketData(const uint8_t *data, size_t size) {
+ const WebSocketMessage *message =
+ flatbuffers::GetRoot<WebSocketMessage>(data);
+ switch (message->payload_type()) {
+ case Payload::WebSocketSdp: {
+ const WebSocketSdp *offer = message->payload_as_WebSocketSdp();
+ if (offer->type() != SdpType::OFFER) {
+ LOG(WARNING) << "Got the wrong sdp type from client";
+ break;
+ }
+ const flatbuffers::String *sdp = offer->payload();
+ webrtc::SdpParseError error;
+ std::unique_ptr<webrtc::SessionDescriptionInterface> desc =
+ CreateSessionDescription(webrtc::SdpType::kOffer, sdp->str(), &error);
+ if (!desc) {
+ LOG(WARNING) << "Failed to parse sdp description: "
+ << error.description;
+ // TODO(alex): send a message back to browser for failure.
+ break;
+ }
+
+ // We can only start creating the PeerConnection once we have something to
+ // give it, so we wait until we get an offer before starting.
+ webrtc::PeerConnectionInterface::RTCConfiguration config;
+ config.sdp_semantics = webrtc::SdpSemantics::kUnifiedPlan;
+ config.enable_dtls_srtp = true;
+
+ std::unique_ptr<rtc::Thread> signaling_thread = rtc::Thread::Create();
+ signaling_thread->SetName("signaling_thread", nullptr);
+ signaling_thread->Start();
+
+ webrtc::PeerConnectionFactoryDependencies factory_deps;
+ factory_deps.signaling_thread = signaling_thread.release();
+ rtc::scoped_refptr<webrtc::PeerConnectionFactoryInterface> factory =
+ CreateModularPeerConnectionFactory(std::move(factory_deps));
+
+ peer_connection_ =
+ factory->CreatePeerConnection(config, nullptr, nullptr, this);
+
+ peer_connection_->SetRemoteDescription(
+ DummySetSessionDescriptionObserver::Create(), desc.release());
+
+ peer_connection_->CreateAnswer(
+ this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
+ break;
+ }
+ case Payload::WebSocketIce: {
+ const WebSocketIce *ice = message->payload_as_WebSocketIce();
+ std::string candidate = ice->candidate()->str();
+ std::string sdpMid = ice->sdpMid()->str();
+ int sdpMLineIndex = ice->sdpMLineIndex();
+ webrtc::SdpParseError error;
+ webrtc::IceCandidateInterface *ice_candidate =
+ webrtc::CreateIceCandidate(sdpMid, sdpMLineIndex, candidate, &error);
+ if (!ice_candidate) {
+ LOG(WARNING) << "Failed to parse ice candidate: " << error.description;
+ // TODO(alex): send a message back to browser for failure.
+ break;
+ }
+ peer_connection_->AddIceCandidate(ice_candidate);
+ break;
+ }
+ default: { break; }
+ }
+}
+
+void Connection::OnDataChannel(
+ rtc::scoped_refptr<webrtc::DataChannelInterface> channel) {
+ data_channel_ = channel;
+ data_channel_->RegisterObserver(this);
+}
+
+void Connection::OnIceCandidate(
+ const webrtc::IceCandidateInterface *candidate) {
+ flatbuffers::FlatBufferBuilder fbb(512);
+ std::string ice_string;
+ candidate->ToString(&ice_string);
+
+ flatbuffers::Offset<WebSocketIce> ice_fb = CreateWebSocketIceDirect(
+ fbb, ice_string.c_str(), candidate->sdp_mid().c_str(),
+ candidate->sdp_mline_index());
+ flatbuffers::Offset<WebSocketMessage> ice_message =
+ CreateWebSocketMessage(fbb, Payload::WebSocketIce, ice_fb.Union());
+ fbb.Finish(ice_message);
+
+ server_->execute(std::make_shared<UpdateData>(sock_, fbb.Release()));
+}
+
+// This is the callback for creating an sdp. We have to manually assign it
+// locally and send it to the client.
+void Connection::OnSuccess(webrtc::SessionDescriptionInterface *desc) {
+ peer_connection_->SetLocalDescription(
+ DummySetSessionDescriptionObserver::Create(), desc);
+ flatbuffers::FlatBufferBuilder fbb(512);
+ std::string answer_string;
+ desc->ToString(&answer_string);
+ flatbuffers::Offset<WebSocketSdp> sdp_fb =
+ CreateWebSocketSdpDirect(fbb, SdpType::ANSWER, answer_string.c_str());
+ flatbuffers::Offset<WebSocketMessage> answer_message =
+ CreateWebSocketMessage(fbb, Payload::WebSocketSdp, sdp_fb.Union());
+ fbb.Finish(answer_message);
+
+ server_->execute(std::make_shared<UpdateData>(sock_, fbb.Release()));
+}
+
+// Receive and respond to a DataChannel message. Temporarily acting as a
+// "PONG", but will change to handle "Connect" subscription messages.
+void Connection::OnMessage(const webrtc::DataBuffer &buffer) {
+ // This is technically disallowed by webrtc, But doesn't seem to cause major
+ // problems. At least for the small data tested manually. Send should be
+ // called from outside this call stack.
+ data_channel_->Send(buffer);
+}
+
+} // namespace web_proxy
+} // namespace aos
diff --git a/aos/network/web_proxy.fbs b/aos/network/web_proxy.fbs
new file mode 100644
index 0000000..e712622
--- /dev/null
+++ b/aos/network/web_proxy.fbs
@@ -0,0 +1,31 @@
+namespace aos.web_proxy;
+
+// SDP is Session Description Protocol. We only handle OFFER (starting a
+// transaction) and ANSWER responding to an offer.
+enum SdpType : byte {
+ OFFER,
+ ANSWER
+}
+
+// The SDP payload is an opaque string that describes what (media/data) we
+// want to transmit.
+table WebSocketSdp {
+ type:SdpType;
+ payload:string;
+}
+
+// ICE is way for different peers to learn how to connect to each other.
+// Because we will only be running in a local network, we don't have to support
+// advaced features.
+table WebSocketIce {
+ candidate:string;
+ sdpMid:string;
+ sdpMLineIndex:int;
+}
+
+union Payload {WebSocketSdp, WebSocketIce}
+
+// We only send a single type of message on the websocket to simplify parsing.
+table WebSocketMessage {
+ payload:Payload;
+}
diff --git a/aos/network/web_proxy.h b/aos/network/web_proxy.h
new file mode 100644
index 0000000..7c24eae
--- /dev/null
+++ b/aos/network/web_proxy.h
@@ -0,0 +1,105 @@
+#ifndef AOS_NETWORK_WEB_PROXY_H_
+#define AOS_NETWORK_WEB_PROXY_H_
+#include <map>
+#include <set>
+#include "aos/seasocks/seasocks_logger.h"
+#include "flatbuffers/flatbuffers.h"
+#include "seasocks/Server.h"
+#include "seasocks/StringUtil.h"
+#include "seasocks/WebSocket.h"
+
+#include "api/peer_connection_interface.h"
+
+namespace aos {
+namespace web_proxy {
+
+class Connection;
+
+// Basic class that handles receiving new websocket connections. Creates a new
+// Connection to manage the rest of the negotiation and data passing. When the
+// websocket closes, it deletes the Connection.
+class WebsocketHandler : public ::seasocks::WebSocket::Handler {
+ public:
+ WebsocketHandler(::seasocks::Server *server);
+ void onConnect(::seasocks::WebSocket *sock) override;
+ void onData(::seasocks::WebSocket *sock, const uint8_t *data,
+ size_t size) override;
+ void onDisconnect(::seasocks::WebSocket *sock) override;
+
+ private:
+ std::map<::seasocks::WebSocket *, std::unique_ptr<Connection>> connections_;
+ ::seasocks::Server *server_;
+};
+
+// Seasocks requires that sends happen on the correct thread. This class takes a
+// detached buffer to send on a specific websocket connection and sends it when
+// seasocks is ready.
+class UpdateData : public ::seasocks::Server::Runnable {
+ public:
+ UpdateData(::seasocks::WebSocket *websocket,
+ ::flatbuffers::DetachedBuffer &&buffer)
+ : sock_(websocket), buffer_(std::move(buffer)) {}
+ ~UpdateData() override = default;
+ UpdateData(const UpdateData &) = delete;
+ UpdateData &operator=(const UpdateData &) = delete;
+
+ void run() override { sock_->send(buffer_.data(), buffer_.size()); }
+
+ private:
+ ::seasocks::WebSocket *sock_;
+ const ::flatbuffers::DetachedBuffer buffer_;
+};
+
+// Represents a single connection to a browser for the entire lifetime of the
+// connection.
+class Connection : public webrtc::PeerConnectionObserver,
+ public webrtc::CreateSessionDescriptionObserver,
+ public webrtc::DataChannelObserver {
+ public:
+ Connection(::seasocks::WebSocket *sock, ::seasocks::Server *server);
+
+ void HandleWebSocketData(const uint8_t *data, size_t size);
+
+ // PeerConnectionObserver implementation
+ void OnSignalingChange(
+ webrtc::PeerConnectionInterface::SignalingState) override {}
+ void OnAddStream(rtc::scoped_refptr<webrtc::MediaStreamInterface>) override {}
+ void OnRemoveStream(
+ rtc::scoped_refptr<webrtc::MediaStreamInterface>) override {}
+ void OnDataChannel(
+ rtc::scoped_refptr<webrtc::DataChannelInterface> channel) override;
+ void OnRenegotiationNeeded() override {}
+ void OnIceConnectionChange(
+ webrtc::PeerConnectionInterface::IceConnectionState state) override {}
+ void OnIceGatheringChange(
+ webrtc::PeerConnectionInterface::IceGatheringState) override {}
+ void OnIceCandidate(const webrtc::IceCandidateInterface *candidate) override;
+ void OnIceConnectionReceivingChange(bool) override {}
+
+ // CreateSessionDescriptionObserver implementation
+ void OnSuccess(webrtc::SessionDescriptionInterface *desc) override;
+ void OnFailure(webrtc::RTCError error) override {}
+ // CreateSessionDescriptionObserver is a refcounted object
+ void AddRef() const override {}
+ // We handle ownership with a unique_ptr so don't worry about actually
+ // refcounting. We will delete when we are done.
+ rtc::RefCountReleaseStatus Release() const override {
+ return rtc::RefCountReleaseStatus::kOtherRefsRemained;
+ }
+
+ // DataChannelObserver implementation
+ void OnStateChange() override {}
+ void OnMessage(const webrtc::DataBuffer &buffer) override;
+ void OnBufferedAmountChange(uint64_t sent_data_size) override {}
+
+ private:
+ ::seasocks::WebSocket *sock_;
+ ::seasocks::Server *server_;
+ rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection_;
+ rtc::scoped_refptr<webrtc::DataChannelInterface> data_channel_;
+};
+
+} // namespace web_proxy
+} // namespace aos
+
+#endif // AOS_NETWORK_WEB_PROXY_H_
diff --git a/aos/network/web_proxy_main.cc b/aos/network/web_proxy_main.cc
new file mode 100644
index 0000000..37b3d1e
--- /dev/null
+++ b/aos/network/web_proxy_main.cc
@@ -0,0 +1,23 @@
+#include "aos/init.h"
+#include "aos/seasocks/seasocks_logger.h"
+#include "aos/network/web_proxy.h"
+
+#include "internal/Embedded.h"
+#include "seasocks/Server.h"
+#include "seasocks/WebSocket.h"
+
+int main() {
+ // Make sure to reference this to force the linker to include it.
+ findEmbeddedContent("");
+
+ aos::InitNRT();
+
+ seasocks::Server server(::std::shared_ptr<seasocks::Logger>(
+ new ::aos::seasocks::SeasocksLogger(seasocks::Logger::Level::Info)));
+
+ auto websocket_handler =
+ std::make_shared<aos::web_proxy::WebsocketHandler>(&server);
+ server.addWebSocketHandler("/ws", websocket_handler);
+
+ server.serve("aos/network/www", 8080);
+}
diff --git a/aos/network/www/BUILD b/aos/network/www/BUILD
new file mode 100644
index 0000000..5faae12
--- /dev/null
+++ b/aos/network/www/BUILD
@@ -0,0 +1,41 @@
+load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
+load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle")
+
+filegroup(
+ name = "files",
+ srcs = glob([
+ "**/*.html",
+ ]),
+ visibility=["//visibility:public"],
+)
+
+ts_library(
+ name = "proxy",
+ srcs = glob([
+ "*.ts",
+ ]),
+ deps = [
+ "//aos/network:web_proxy_ts_fbs",
+ ],
+)
+
+rollup_bundle(
+ name = "proxy_bundle",
+ entry_point = "aos/network/www/main",
+ deps = [
+ "proxy",
+ ],
+ visibility=["//visibility:public"],
+)
+
+genrule(
+ name = "flatbuffers",
+ srcs = [
+ "@com_github_google_flatbuffers//:flatjs",
+ ],
+ outs = [
+ "flatbuffers.js",
+ ],
+ cmd = "cp $(location @com_github_google_flatbuffers//:flatjs) $@",
+ visibility=["//visibility:public"],
+)
diff --git a/aos/network/www/index.html b/aos/network/www/index.html
new file mode 100644
index 0000000..bc90d40
--- /dev/null
+++ b/aos/network/www/index.html
@@ -0,0 +1,6 @@
+<html>
+ <body>
+ <script src="flatbuffers.js"></script>
+ <script src="proxy_bundle.min.js"></script>
+ </body>
+</html>
diff --git a/aos/network/www/main.ts b/aos/network/www/main.ts
new file mode 100644
index 0000000..5a3165e
--- /dev/null
+++ b/aos/network/www/main.ts
@@ -0,0 +1,5 @@
+import {Connection} from './proxy';
+
+const conn = new Connection();
+
+conn.connect();
diff --git a/aos/network/www/proxy.ts b/aos/network/www/proxy.ts
new file mode 100644
index 0000000..1ef6320
--- /dev/null
+++ b/aos/network/www/proxy.ts
@@ -0,0 +1,114 @@
+import {aos.web_proxy} from '../web_proxy_generated';
+
+// Analogous to the Connection class in //aos/network/web_proxy.h. Because most
+// of the apis are native in JS, it is much simpler.
+export class Connection {
+ private webSocketConnection: WebSocket|null = null;
+ private rtcPeerConnection: RTCPeerConnection|null = null;
+ private dataChannel: DataChannel|null = null;
+ private webSocketUrl: string;
+
+ constructor() {
+ const server = location.host;
+ this.webSocketUrl = `ws://${server}/ws`;
+ }
+
+ connect(): void {
+ this.webSocketConnection = new WebSocket(this.webSocketUrl);
+ this.webSocketConnection.binaryType = 'arraybuffer';
+ this.webSocketConnection.addEventListener(
+ 'open', () => this.onWebSocketOpen());
+ this.webSocketConnection.addEventListener(
+ 'message', (e) => this.onWebSocketMessage(e));
+ }
+
+ // Handle messages on the DataChannel. Will delegate to various handlers for
+ // different message types.
+ onDataChannelMessage(e: MessageEvent): void {
+ console.log(e);
+ }
+
+ onIceCandidate(e: RTCPeerConnectionIceEvent): void {
+ console.log('Created ice candidate', e);
+ if (!e.candidate) {
+ return;
+ }
+ const candidate = e.candidate;
+ const builder = new flatbuffers.Builder(512);
+ const candidateString = builder.createString(candidate.candidate);
+ const sdpMidString = builder.createString(candidate.sdpMid);
+
+ const iceFb = aos.web_proxy.WebSocketIce.createWebSocketIce(
+ builder, candidateString, sdpMidString, candidate.sdpMLineIndex);
+ const messageFb = aos.web_proxy.WebSocketMessage.createWebSocketMessage(
+ builder, aos.web_proxy.Payload.WebSocketIce, iceFb);
+ builder.finish(messageFb);
+ const array = builder.asUint8Array();
+ this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
+ }
+
+ // Called for new SDPs. Make sure to set it locally and remotely.
+ onOfferCreated(description: RTCSessionDescription): void {
+ console.log('Created offer', description);
+ this.rtcPeerConnection.setLocalDescription(description);
+ const builder = new flatbuffers.Builder(512);
+ const offerString = builder.createString(description.sdp);
+
+ const webSocketSdp = aos.web_proxy.WebSocketSdp.createWebSocketSdp(
+ builder, aos.web_proxy.SdpType.OFFER, offerString);
+ const message = aos.web_proxy.WebSocketMessage.createWebSocketMessage(
+ builder, aos.web_proxy.Payload.WebSocketSdp, webSocketSdp);
+ builder.finish(message);
+ const array = builder.asUint8Array();
+ this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
+ }
+
+ // We now have a websocket, so start setting up the peer connection. We only
+ // want a DataChannel, so create it and then create an offer to send.
+ onWebSocketOpen(): void {
+ this.rtcPeerConnection = new RTCPeerConnection({});
+ this.dataChannel = this.rtcPeerConnection.createDataChannel('dc');
+ this.dataChannel.addEventListener(
+ 'message', (e) => this.onDataChannelMessage(e));
+ window.dc = this.dataChannel;
+ this.rtcPeerConnection.addEventListener(
+ 'icecandidate', (e) => this.onIceCandidate(e));
+ this.rtcPeerConnection.createOffer().then(
+ (offer) => this.onOfferCreated(offer));
+ }
+
+ // When we receive a websocket message, we need to determine what type it is
+ // and handle appropriately. Either by setting the remote description or
+ // adding the remote ice candidate.
+ onWebSocketMessage(e: MessageEvent): void {
+ console.log('ws: ', e);
+ const buffer = new Uint8Array(e.data)
+ const fbBuffer = new flatbuffers.ByteBuffer(buffer);
+ const message =
+ aos.web_proxy.WebSocketMessage.getRootAsWebSocketMessage(fbBuffer);
+ switch (message.payloadType()) {
+ case aos.web_proxy.Payload.WebSocketSdp:
+ console.log('got an sdp message');
+ const sdpFb = message.payload(new aos.web_proxy.WebSocketSdp());
+ if (sdpFb.type() !== aos.web_proxy.SdpType.ANSWER) {
+ console.log('got something other than an answer back');
+ break;
+ }
+ this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(
+ {'type': 'answer', 'sdp': sdpFb.payload()}));
+ break;
+ case aos.web_proxy.Payload.WebSocketIce:
+ console.log('got an ice message');
+ const iceFb = message.payload(new aos.web_proxy.WebSocketIce());
+ const candidate = {} as RTCIceCandidateInit;
+ candidate.candidate = iceFb.candidate();
+ candidate.sdpMid = iceFb.sdpMid();
+ candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
+ this.rtcPeerConnection.addIceCandidate(candidate);
+ break;
+ default:
+ console.log('got an unknown message');
+ break;
+ }
+ }
+}
diff --git a/aos/network/www_defaults/_404.png b/aos/network/www_defaults/_404.png
new file mode 100644
index 0000000..8a43cb8
--- /dev/null
+++ b/aos/network/www_defaults/_404.png
Binary files differ
diff --git a/aos/network/www_defaults/_error.css b/aos/network/www_defaults/_error.css
new file mode 100644
index 0000000..8238d6d
--- /dev/null
+++ b/aos/network/www_defaults/_error.css
@@ -0,0 +1,33 @@
+body {
+ font-family: segoe ui, tahoma, arial, sans-serif;
+ color: #ffffff;
+ background-color: #c21e29;
+ text-align: center;
+}
+
+a {
+ color: #ffff00;
+}
+
+.footer {
+ font-style: italic;
+}
+
+.message {
+ display: inline-block;
+ border: 1px solid white;
+ padding: 50px;
+ font-size: 20px;
+}
+
+.headline {
+ padding: 50px;
+ font-weight: bold;
+ font-size: 32px;
+}
+
+.footer {
+ padding-top: 50px;
+ font-size: 12px;
+}
+
diff --git a/aos/network/www_defaults/_error.html b/aos/network/www_defaults/_error.html
new file mode 100644
index 0000000..ecf5e32
--- /dev/null
+++ b/aos/network/www_defaults/_error.html
@@ -0,0 +1,15 @@
+<html DOCTYPE=html>
+<head>
+ <title>%%ERRORCODE%% - %%MESSAGE%% - Keep Calm And Carry On!</title>
+ <link href="/_error.css" rel="stylesheet">
+</head>
+<body>
+ <div class="message">
+ <img src="/_404.png" height="200" width="107">
+ <div class="headline">%%ERRORCODE%% — %%MESSAGE%%</div>
+ <div class="info">%%BODY%%</div>
+ </div>
+
+ <div class="footer">Powered by <a href="https://github.com/mattgodbolt/seasocks">SeaSocks</a></div>
+</body>
+</html>
diff --git a/aos/network/www_defaults/_jquery.min.js b/aos/network/www_defaults/_jquery.min.js
new file mode 100644
index 0000000..f78f96a
--- /dev/null
+++ b/aos/network/www_defaults/_jquery.min.js
@@ -0,0 +1,16 @@
+/*!
+ * jQuery JavaScript Library v1.5.2
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Thu Mar 31 15:28:23 2011 -0400
+ */
+(function(a,b){function ci(a){return d.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cf(a){if(!b_[a]){var b=d("<"+a+">").appendTo("body"),c=b.css("display");b.remove();if(c==="none"||c==="")c="block";b_[a]=c}return b_[a]}function ce(a,b){var c={};d.each(cd.concat.apply([],cd.slice(0,b)),function(){c[this]=a});return c}function b$(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function bZ(){try{return new a.XMLHttpRequest}catch(b){}}function bY(){d(a).unload(function(){for(var a in bW)bW[a](0,1)})}function bS(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var e=a.dataTypes,f={},g,h,i=e.length,j,k=e[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h==="string"&&(f[h.toLowerCase()]=a.converters[h]);l=k,k=e[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=f[m]||f["* "+k];if(!n){p=b;for(o in f){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=f[j[1]+" "+k];if(p){o=f[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&d.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bR(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bQ(a,b,c,e){if(d.isArray(b)&&b.length)d.each(b,function(b,f){c||bs.test(a)?e(a,f):bQ(a+"["+(typeof f==="object"||d.isArray(f)?b:"")+"]",f,c,e)});else if(c||b==null||typeof b!=="object")e(a,b);else if(d.isArray(b)||d.isEmptyObject(b))e(a,"");else for(var f in b)bQ(a+"["+f+"]",b[f],c,e)}function bP(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bJ,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l==="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bP(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bP(a,c,d,e,"*",g));return l}function bO(a){return function(b,c){typeof b!=="string"&&(c=b,b="*");if(d.isFunction(c)){var e=b.toLowerCase().split(bD),f=0,g=e.length,h,i,j;for(;f<g;f++)h=e[f],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bq(a,b,c){var e=b==="width"?bk:bl,f=b==="width"?a.offsetWidth:a.offsetHeight;if(c==="border")return f;d.each(e,function(){c||(f-=parseFloat(d.css(a,"padding"+this))||0),c==="margin"?f+=parseFloat(d.css(a,"margin"+this))||0:f-=parseFloat(d.css(a,"border"+this+"Width"))||0});return f}function bc(a,b){b.src?d.ajax({url:b.src,async:!1,dataType:"script"}):d.globalEval(b.text||b.textContent||b.innerHTML||""),b.parentNode&&b.parentNode.removeChild(b)}function bb(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function ba(a,b){if(b.nodeType===1){var c=b.nodeName.toLowerCase();b.clearAttributes(),b.mergeAttributes(a);if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(d.expando)}}function _(a,b){if(b.nodeType===1&&d.hasData(a)){var c=d.expando,e=d.data(a),f=d.data(b,e);if(e=e[c]){var g=e.events;f=f[c]=d.extend({},e);if(g){delete f.handle,f.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)d.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function $(a,b){return d.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Q(a,b,c){if(d.isFunction(b))return d.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return d.grep(a,function(a,d){return a===b===c});if(typeof b==="string"){var e=d.grep(a,function(a){return a.nodeType===1});if(L.test(b))return d.filter(b,e,!c);b=d.filter(b,e)}return d.grep(a,function(a,e){return d.inArray(a,b)>=0===c})}function P(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function H(a,b){return(a&&a!=="*"?a+".":"")+b.replace(t,"`").replace(u,"&")}function G(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,p=[],q=[],s=d._data(this,"events");if(a.liveFired!==this&&s&&s.live&&!a.target.disabled&&(!a.button||a.type!=="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var t=s.live.slice(0);for(i=0;i<t.length;i++)g=t[i],g.origType.replace(r,"")===a.type?q.push(g.selector):t.splice(i--,1);f=d(a.target).closest(q,a.currentTarget);for(j=0,k=f.length;j<k;j++){m=f[j];for(i=0;i<t.length;i++){g=t[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,e=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,e=d(a.relatedTarget).closest(g.selector)[0];(!e||e!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){f=p[j];if(c&&f.level>c)break;a.currentTarget=f.elem,a.data=f.handleObj.data,a.handleObj=f.handleObj,o=f.handleObj.origHandler.apply(f.elem,arguments);if(o===!1||a.isPropagationStopped()){c=f.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function E(a,c,e){var f=d.extend({},e[0]);f.type=a,f.originalEvent={},f.liveFired=b,d.event.handle.call(c,f),f.isDefaultPrevented()&&e[0].preventDefault()}function y(){return!0}function x(){return!1}function i(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function h(a,c,e){if(e===b&&a.nodeType===1){e=a.getAttribute("data-"+c);if(typeof e==="string"){try{e=e==="true"?!0:e==="false"?!1:e==="null"?null:d.isNaN(e)?g.test(e)?d.parseJSON(e):e:parseFloat(e)}catch(f){}d.data(a,c,e)}else e=b}return e}var c=a.document,d=function(){function G(){if(!d.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(G,1);return}d.ready()}}var d=function(a,b){return new d.fn.init(a,b,g)},e=a.jQuery,f=a.$,g,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,i=/\S/,j=/^\s+/,k=/\s+$/,l=/\d/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=navigator.userAgent,w,x,y,z=Object.prototype.toString,A=Object.prototype.hasOwnProperty,B=Array.prototype.push,C=Array.prototype.slice,D=String.prototype.trim,E=Array.prototype.indexOf,F={};d.fn=d.prototype={constructor:d,init:function(a,e,f){var g,i,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!e&&c.body){this.context=c,this[0]=c.body,this.selector="body",this.length=1;return this}if(typeof a==="string"){g=h.exec(a);if(!g||!g[1]&&e)return!e||e.jquery?(e||f).find(a):this.constructor(e).find(a);if(g[1]){e=e instanceof d?e[0]:e,k=e?e.ownerDocument||e:c,j=m.exec(a),j?d.isPlainObject(e)?(a=[c.createElement(j[1])],d.fn.attr.call(a,e,!0)):a=[k.createElement(j[1])]:(j=d.buildFragment([g[1]],[k]),a=(j.cacheable?d.clone(j.fragment):j.fragment).childNodes);return d.merge(this,a)}i=c.getElementById(g[2]);if(i&&i.parentNode){if(i.id!==g[2])return f.find(a);this.length=1,this[0]=i}this.context=c,this.selector=a;return this}if(d.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return d.makeArray(a,this)},selector:"",jquery:"1.5.2",length:0,size:function(){return this.length},toArray:function(){return C.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var e=this.constructor();d.isArray(a)?B.apply(e,a):d.merge(e,a),e.prevObject=this,e.context=this.context,b==="find"?e.selector=this.selector+(this.selector?" ":"")+c:b&&(e.selector=this.selector+"."+b+"("+c+")");return e},each:function(a,b){return d.each(this,a,b)},ready:function(a){d.bindReady(),x.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(C.apply(this,arguments),"slice",C.call(arguments).join(","))},map:function(a){return this.pushStack(d.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:B,sort:[].sort,splice:[].splice},d.fn.init.prototype=d.fn,d.extend=d.fn.extend=function(){var a,c,e,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i==="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!=="object"&&!d.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){e=i[c],f=a[c];if(i===f)continue;l&&f&&(d.isPlainObject(f)||(g=d.isArray(f)))?(g?(g=!1,h=e&&d.isArray(e)?e:[]):h=e&&d.isPlainObject(e)?e:{},i[c]=d.extend(l,h,f)):f!==b&&(i[c]=f)}return i},d.extend({noConflict:function(b){a.$=f,b&&(a.jQuery=e);return d},isReady:!1,readyWait:1,ready:function(a){a===!0&&d.readyWait--;if(!d.readyWait||a!==!0&&!d.isReady){if(!c.body)return setTimeout(d.ready,1);d.isReady=!0;if(a!==!0&&--d.readyWait>0)return;x.resolveWith(c,[d]),d.fn.trigger&&d(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!x){x=d._Deferred();if(c.readyState==="complete")return setTimeout(d.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",y,!1),a.addEventListener("load",d.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",y),a.attachEvent("onload",d.ready);var b=!1;try{b=a.frameElement==null}catch(e){}c.documentElement.doScroll&&b&&G()}}},isFunction:function(a){return d.type(a)==="function"},isArray:Array.isArray||function(a){return d.type(a)==="array"},isWindow:function(a){return a&&typeof a==="object"&&"setInterval"in a},isNaN:function(a){return a==null||!l.test(a)||isNaN(a)},type:function(a){return a==null?String(a):F[z.call(a)]||"object"},isPlainObject:function(a){if(!a||d.type(a)!=="object"||a.nodeType||d.isWindow(a))return!1;if(a.constructor&&!A.call(a,"constructor")&&!A.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a){}return c===b||A.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!=="string"||!b)return null;b=d.trim(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return a.JSON&&a.JSON.parse?a.JSON.parse(b):(new Function("return "+b))();d.error("Invalid JSON: "+b)},parseXML:function(b,c,e){a.DOMParser?(e=new DOMParser,c=e.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),e=c.documentElement,(!e||!e.nodeName||e.nodeName==="parsererror")&&d.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(a){if(a&&i.test(a)){var b=c.head||c.getElementsByTagName("head")[0]||c.documentElement,e=c.createElement("script");d.support.scriptEval()?e.appendChild(c.createTextNode(a)):e.text=a,b.insertBefore(e,b.firstChild),b.removeChild(e)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,e){var f,g=0,h=a.length,i=h===b||d.isFunction(a);if(e){if(i){for(f in a)if(c.apply(a[f],e)===!1)break}else for(;g<h;)if(c.apply(a[g++],e)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(var j=a[0];g<h&&c.call(j,g,j)!==!1;j=a[++g]){}return a},trim:D?function(a){return a==null?"":D.call(a)}:function(a){return a==null?"":(a+"").replace(j,"").replace(k,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var e=d.type(a);a.length==null||e==="string"||e==="function"||e==="regexp"||d.isWindow(a)?B.call(c,a):d.merge(c,a)}return c},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length==="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,b,c){var d=[],e;for(var f=0,g=a.length;f<g;f++)e=b(a[f],f,c),e!=null&&(d[d.length]=e);return d.concat.apply([],d)},guid:1,proxy:function(a,c,e){arguments.length===2&&(typeof c==="string"?(e=a,a=e[c],c=b):c&&!d.isFunction(c)&&(e=c,c=b)),!c&&a&&(c=function(){return a.apply(e||this,arguments)}),a&&(c.guid=a.guid=a.guid||c.guid||d.guid++);return c},access:function(a,c,e,f,g,h){var i=a.length;if(typeof c==="object"){for(var j in c)d.access(a,j,c[j],f,g,e);return a}if(e!==b){f=!h&&f&&d.isFunction(e);for(var k=0;k<i;k++)g(a[k],c,f?e.call(a[k],k,g(a[k],c)):e,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}d.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.subclass=this.subclass,a.fn.init=function b(b,c){c&&c instanceof d&&!(c instanceof a)&&(c=a(c));return d.fn.init.call(this,b,c,e)},a.fn.init.prototype=a.fn;var e=a(c);return a},browser:{}}),d.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){F["[object "+b+"]"]=b.toLowerCase()}),w=d.uaMatch(v),w.browser&&(d.browser[w.browser]=!0,d.browser.version=w.version),d.browser.webkit&&(d.browser.safari=!0),E&&(d.inArray=function(a,b){return E.call(b,a)}),i.test(" ")&&(j=/^[\s\xA0]+/,k=/[\s\xA0]+$/),g=d(c),c.addEventListener?y=function(){c.removeEventListener("DOMContentLoaded",y,!1),d.ready()}:c.attachEvent&&(y=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",y),d.ready())});return d}(),e="then done fail isResolved isRejected promise".split(" "),f=[].slice;d.extend({_Deferred:function(){var a=[],b,c,e,f={done:function(){if(!e){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=d.type(i),j==="array"?f.done.apply(f,i):j==="function"&&a.push(i);k&&f.resolveWith(k[0],k[1])}return this},resolveWith:function(d,f){if(!e&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(d,f)}finally{b=[d,f],c=0}}return this},resolve:function(){f.resolveWith(this,arguments);return this},isResolved:function(){return c||b},cancel:function(){e=1,a=[];return this}};return f},Deferred:function(a){var b=d._Deferred(),c=d._Deferred(),f;d.extend(b,{then:function(a,c){b.done(a).fail(c);return this},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,promise:function(a){if(a==null){if(f)return f;f=a={}}var c=e.length;while(c--)a[e[c]]=b[e[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?f.call(arguments,0):c,--g||h.resolveWith(h,f.call(b,0))}}var b=arguments,c=0,e=b.length,g=e,h=e<=1&&a&&d.isFunction(a.promise)?a:d.Deferred();if(e>1){for(;c<e;c++)b[c]&&d.isFunction(b[c].promise)?b[c].promise().then(i(c),h.reject):--g;g||h.resolveWith(h,b)}else h!==a&&h.resolveWith(h,e?[a]:[]);return h.promise()}}),function(){d.support={};var b=c.createElement("div");b.style.display="none",b.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=b.getElementsByTagName("*"),f=b.getElementsByTagName("a")[0],g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=b.getElementsByTagName("input")[0];if(e&&e.length&&f){d.support={leadingWhitespace:b.firstChild.nodeType===3,tbody:!b.getElementsByTagName("tbody").length,htmlSerialize:!!b.getElementsByTagName("link").length,style:/red/.test(f.getAttribute("style")),hrefNormalized:f.getAttribute("href")==="/a",opacity:/^0.55$/.test(f.style.opacity),cssFloat:!!f.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,deleteExpando:!0,optDisabled:!1,checkClone:!1,noCloneEvent:!0,noCloneChecked:!0,boxModel:null,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableHiddenOffsets:!0,reliableMarginRight:!0},i.checked=!0,d.support.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,d.support.optDisabled=!h.disabled;var j=null;d.support.scriptEval=function(){if(j===null){var b=c.documentElement,e=c.createElement("script"),f="script"+d.now();try{e.appendChild(c.createTextNode("window."+f+"=1;"))}catch(g){}b.insertBefore(e,b.firstChild),a[f]?(j=!0,delete a[f]):j=!1,b.removeChild(e)}return j};try{delete b.test}catch(k){d.support.deleteExpando=!1}!b.addEventListener&&b.attachEvent&&b.fireEvent&&(b.attachEvent("onclick",function l(){d.support.noCloneEvent=!1,b.detachEvent("onclick",l)}),b.cloneNode(!0).fireEvent("onclick")),b=c.createElement("div"),b.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";var m=c.createDocumentFragment();m.appendChild(b.firstChild),d.support.checkClone=m.cloneNode(!0).cloneNode(!0).lastChild.checked,d(function(){var a=c.createElement("div"),b=c.getElementsByTagName("body")[0];if(b){a.style.width=a.style.paddingLeft="1px",b.appendChild(a),d.boxModel=d.support.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,d.support.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>";var e=a.getElementsByTagName("td");d.support.reliableHiddenOffsets=e[0].offsetHeight===0,e[0].style.display="",e[1].style.display="none",d.support.reliableHiddenOffsets=d.support.reliableHiddenOffsets&&e[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(a.style.width="1px",a.style.marginRight="0",d.support.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(a,null).marginRight,10)||0)===0),b.removeChild(a).style.display="none",a=e=null}});var n=function(a){var b=c.createElement("div");a="on"+a;if(!b.attachEvent)return!0;var d=a in b;d||(b.setAttribute(a,"return;"),d=typeof b[a]==="function");return d};d.support.submitBubbles=n("submit"),d.support.changeBubbles=n("change"),b=e=f=null}}();var g=/^(?:\{.*\}|\[.*\])$/;d.extend({cache:{},uuid:0,expando:"jQuery"+(d.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?d.cache[a[d.expando]]:a[d.expando];return!!a&&!i(a)},data:function(a,c,e,f){if(d.acceptData(a)){var g=d.expando,h=typeof c==="string",i,j=a.nodeType,k=j?d.cache:a,l=j?a[d.expando]:a[d.expando]&&d.expando;if((!l||f&&l&&!k[l][g])&&h&&e===b)return;l||(j?a[d.expando]=l=++d.uuid:l=d.expando),k[l]||(k[l]={},j||(k[l].toJSON=d.noop));if(typeof c==="object"||typeof c==="function")f?k[l][g]=d.extend(k[l][g],c):k[l]=d.extend(k[l],c);i=k[l],f&&(i[g]||(i[g]={}),i=i[g]),e!==b&&(i[c]=e);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,e){if(d.acceptData(b)){var f=d.expando,g=b.nodeType,h=g?d.cache:b,j=g?b[d.expando]:d.expando;if(!h[j])return;if(c){var k=e?h[j][f]:h[j];if(k){delete k[c];if(!i(k))return}}if(e){delete h[j][f];if(!i(h[j]))return}var l=h[j][f];d.support.deleteExpando||h!=a?delete h[j]:h[j]=null,l?(h[j]={},g||(h[j].toJSON=d.noop),h[j][f]=l):g&&(d.support.deleteExpando?delete b[d.expando]:b.removeAttribute?b.removeAttribute(d.expando):b[d.expando]=null)}},_data:function(a,b,c){return d.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=d.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),d.fn.extend({data:function(a,c){var e=null;if(typeof a==="undefined"){if(this.length){e=d.data(this[0]);if(this[0].nodeType===1){var f=this[0].attributes,g;for(var i=0,j=f.length;i<j;i++)g=f[i].name,g.indexOf("data-")===0&&(g=g.substr(5),h(this[0],g,e[g]))}}return e}if(typeof a==="object")return this.each(function(){d.data(this,a)});var k=a.split(".");k[1]=k[1]?"."+k[1]:"";if(c===b){e=this.triggerHandler("getData"+k[1]+"!",[k[0]]),e===b&&this.length&&(e=d.data(this[0],a),e=h(this[0],a,e));return e===b&&k[1]?this.data(k[0]):e}return this.each(function(){var b=d(this),e=[k[0],c];b.triggerHandler("setData"+k[1]+"!",e),d.data(this,a,c),b.triggerHandler("changeData"+k[1]+"!",e)})},removeData:function(a){return this.each(function(){d.removeData(this,a)})}}),d.extend({queue:function(a,b,c){if(a){b=(b||"fx")+"queue";var e=d._data(a,b);if(!c)return e||[];!e||d.isArray(c)?e=d._data(a,b,d.makeArray(c)):e.push(c);return e}},dequeue:function(a,b){b=b||"fx";var c=d.queue(a,b),e=c.shift();e==="inprogress"&&(e=c.shift()),e&&(b==="fx"&&c.unshift("inprogress"),e.call(a,function(){d.dequeue(a,b)})),c.length||d.removeData(a,b+"queue",!0)}}),d.fn.extend({queue:function(a,c){typeof a!=="string"&&(c=a,a="fx");if(c===b)return d.queue(this[0],a);return this.each(function(b){var e=d.queue(this,a,c);a==="fx"&&e[0]!=="inprogress"&&d.dequeue(this,a)})},dequeue:function(a){return this.each(function(){d.dequeue(this,a)})},delay:function(a,b){a=d.fx?d.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){d.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var j=/[\n\t\r]/g,k=/\s+/,l=/\r/g,m=/^(?:href|src|style)$/,n=/^(?:button|input)$/i,o=/^(?:button|input|object|select|textarea)$/i,p=/^a(?:rea)?$/i,q=/^(?:radio|checkbox)$/i;d.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"},d.fn.extend({attr:function(a,b){return d.access(this,a,b,!0,d.attr)},removeAttr:function(a,b){return this.each(function(){d.attr(this,a,""),this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.addClass(a.call(this,b,c.attr("class")))});if(a&&typeof a==="string"){var b=(a||"").split(k);for(var c=0,e=this.length;c<e;c++){var f=this[c];if(f.nodeType===1)if(f.className){var g=" "+f.className+" ",h=f.className;for(var i=0,j=b.length;i<j;i++)g.indexOf(" "+b[i]+" ")<0&&(h+=" "+b[i]);f.className=d.trim(h)}else f.className=a}}return this},removeClass:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.removeClass(a.call(this,b,c.attr("class")))});if(a&&typeof a==="string"||a===b){var c=(a||"").split(k);for(var e=0,f=this.length;e<f;e++){var g=this[e];if(g.nodeType===1&&g.className)if(a){var h=(" "+g.className+" ").replace(j," ");for(var i=0,l=c.length;i<l;i++)h=h.replace(" "+c[i]+" "," ");g.className=d.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,e=typeof b==="boolean";if(d.isFunction(a))return this.each(function(c){var e=d(this);e.toggleClass(a.call(this,c,e.attr("class"),b),b)});return this.each(function(){if(c==="string"){var f,g=0,h=d(this),i=b,j=a.split(k);while(f=j[g++])i=e?i:!h.hasClass(f),h[i?"addClass":"removeClass"](f)}else if(c==="undefined"||c==="boolean")this.className&&d._data(this,"__className__",this.className),this.className=this.className||a===!1?"":d._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(j," ").indexOf(b)>-1)return!0;return!1},val:function(a){if(!arguments.length){var c=this[0];if(c){if(d.nodeName(c,"option")){var e=c.attributes.value;return!e||e.specified?c.value:c.text}if(d.nodeName(c,"select")){var f=c.selectedIndex,g=[],h=c.options,i=c.type==="select-one";if(f<0)return null;for(var j=i?f:0,k=i?f+1:h.length;j<k;j++){var m=h[j];if(m.selected&&(d.support.optDisabled?!m.disabled:m.getAttribute("disabled")===null)&&(!m.parentNode.disabled||!d.nodeName(m.parentNode,"optgroup"))){a=d(m).val();if(i)return a;g.push(a)}}if(i&&!g.length&&h.length)return d(h[f]).val();return g}if(q.test(c.type)&&!d.support.checkOn)return c.getAttribute("value")===null?"on":c.value;return(c.value||"").replace(l,"")}return b}var n=d.isFunction(a);return this.each(function(b){var c=d(this),e=a;if(this.nodeType===1){n&&(e=a.call(this,b,c.val())),e==null?e="":typeof e==="number"?e+="":d.isArray(e)&&(e=d.map(e,function(a){return a==null?"":a+""}));if(d.isArray(e)&&q.test(this.type))this.checked=d.inArray(c.val(),e)>=0;else if(d.nodeName(this,"select")){var f=d.makeArray(e);d("option",this).each(function(){this.selected=d.inArray(d(this).val(),f)>=0}),f.length||(this.selectedIndex=-1)}else this.value=e}})}}),d.extend({attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,e,f){if(!a||a.nodeType===3||a.nodeType===8||a.nodeType===2)return b;if(f&&c in d.attrFn)return d(a)[c](e);var g=a.nodeType!==1||!d.isXMLDoc(a),h=e!==b;c=g&&d.props[c]||c;if(a.nodeType===1){var i=m.test(c);if(c==="selected"&&!d.support.optSelected){var j=a.parentNode;j&&(j.selectedIndex,j.parentNode&&j.parentNode.selectedIndex)}if((c in a||a[c]!==b)&&g&&!i){h&&(c==="type"&&n.test(a.nodeName)&&a.parentNode&&d.error("type property can't be changed"),e===null?a.nodeType===1&&a.removeAttribute(c):a[c]=e);if(d.nodeName(a,"form")&&a.getAttributeNode(c))return a.getAttributeNode(c).nodeValue;if(c==="tabIndex"){var k=a.getAttributeNode("tabIndex");return k&&k.specified?k.value:o.test(a.nodeName)||p.test(a.nodeName)&&a.href?0:b}return a[c]}if(!d.support.style&&g&&c==="style"){h&&(a.style.cssText=""+e);return a.style.cssText}h&&a.setAttribute(c,""+e);if(!a.attributes[c]&&(a.hasAttribute&&!a.hasAttribute(c)))return b;var l=!d.support.hrefNormalized&&g&&i?a.getAttribute(c,2):a.getAttribute(c);return l===null?b:l}h&&(a[c]=e);return a[c]}});var r=/\.(.*)$/,s=/^(?:textarea|input|select)$/i,t=/\./g,u=/ /g,v=/[^\w\s.|`]/g,w=function(a){return a.replace(v,"\\$&")};d.event={add:function(c,e,f,g){if(c.nodeType!==3&&c.nodeType!==8){try{d.isWindow(c)&&(c!==a&&!c.frameElement)&&(c=a)}catch(h){}if(f===!1)f=x;else if(!f)return;var i,j;f.handler&&(i=f,f=i.handler),f.guid||(f.guid=d.guid++);var k=d._data(c);if(!k)return;var l=k.events,m=k.handle;l||(k.events=l={}),m||(k.handle=m=function(a){return typeof d!=="undefined"&&d.event.triggered!==a.type?d.event.handle.apply(m.elem,arguments):b}),m.elem=c,e=e.split(" ");var n,o=0,p;while(n=e[o++]){j=i?d.extend({},i):{handler:f,data:g},n.indexOf(".")>-1?(p=n.split("."),n=p.shift(),j.namespace=p.slice(0).sort().join(".")):(p=[],j.namespace=""),j.type=n,j.guid||(j.guid=f.guid);var q=l[n],r=d.event.special[n]||{};if(!q){q=l[n]=[];if(!r.setup||r.setup.call(c,g,p,m)===!1)c.addEventListener?c.addEventListener(n,m,!1):c.attachEvent&&c.attachEvent("on"+n,m)}r.add&&(r.add.call(c,j),j.handler.guid||(j.handler.guid=f.guid)),q.push(j),d.event.global[n]=!0}c=null}},global:{},remove:function(a,c,e,f){if(a.nodeType!==3&&a.nodeType!==8){e===!1&&(e=x);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=d.hasData(a)&&d._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(e=c.handler,c=c.type);if(!c||typeof c==="string"&&c.charAt(0)==="."){c=c||"";for(h in t)d.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+d.map(m.slice(0).sort(),w).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!e){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))d.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=d.event.special[h]||{};for(j=f||0;j<p.length;j++){q=p[j];if(e.guid===q.guid){if(l||n.test(q.namespace))f==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(f!=null)break}}if(p.length===0||f!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&d.removeEvent(a,h,s.handle),g=null,delete t[h]}if(d.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,d.isEmptyObject(s)&&d.removeData(a,b,!0)}}},trigger:function(a,c,e){var f=a.type||a,g=arguments[3];if(!g){a=typeof a==="object"?a[d.expando]?a:d.extend(d.Event(f),a):d.Event(f),f.indexOf("!")>=0&&(a.type=f=f.slice(0,-1),a.exclusive=!0),e||(a.stopPropagation(),d.event.global[f]&&d.each(d.cache,function(){var b=d.expando,e=this[b];e&&e.events&&e.events[f]&&d.event.trigger(a,c,e.handle.elem)}));if(!e||e.nodeType===3||e.nodeType===8)return b;a.result=b,a.target=e,c=d.makeArray(c),c.unshift(a)}a.currentTarget=e;var h=d._data(e,"handle");h&&h.apply(e,c);var i=e.parentNode||e.ownerDocument;try{e&&e.nodeName&&d.noData[e.nodeName.toLowerCase()]||e["on"+f]&&e["on"+f].apply(e,c)===!1&&(a.result=!1,a.preventDefault())}catch(j){}if(!a.isPropagationStopped()&&i)d.event.trigger(a,c,i,!0);else if(!a.isDefaultPrevented()){var k,l=a.target,m=f.replace(r,""),n=d.nodeName(l,"a")&&m==="click",o=d.event.special[m]||{};if((!o._default||o._default.call(e,a)===!1)&&!n&&!(l&&l.nodeName&&d.noData[l.nodeName.toLowerCase()])){try{l[m]&&(k=l["on"+m],k&&(l["on"+m]=null),d.event.triggered=a.type,l[m]())}catch(p){}k&&(l["on"+m]=k),d.event.triggered=b}}},handle:function(c){var e,f,g,h,i,j=[],k=d.makeArray(arguments);c=k[0]=d.event.fix(c||a.event),c.currentTarget=this,e=c.type.indexOf(".")<0&&!c.exclusive,e||(g=c.type.split("."),c.type=g.shift(),j=g.slice(0).sort(),h=new RegExp("(^|\\.)"+j.join("\\.(?:.*\\.)?")+"(\\.|$)")),c.namespace=c.namespace||j.join("."),i=d._data(this,"events"),f=(i||{})[c.type];if(i&&f){f=f.slice(0);for(var l=0,m=f.length;l<m;l++){var n=f[l];if(e||h.test(n.namespace)){c.handler=n.handler,c.data=n.data,c.handleObj=n;var o=n.handler.apply(this,k);o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[d.expando])return a;var e=a;a=d.Event(e);for(var f=this.props.length,g;f;)g=this.props[--f],a[g]=e[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=c.documentElement,i=c.body;a.pageX=a.clientX+(h&&h.scrollLeft||i&&i.scrollLeft||0)-(h&&h.clientLeft||i&&i.clientLeft||0),a.pageY=a.clientY+(h&&h.scrollTop||i&&i.scrollTop||0)-(h&&h.clientTop||i&&i.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:d.proxy,special:{ready:{setup:d.bindReady,teardown:d.noop},live:{add:function(a){d.event.add(this,H(a.origType,a.selector),d.extend({},a,{handler:G,guid:a.handler.guid}))},remove:function(a){d.event.remove(this,H(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){d.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},d.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},d.Event=function(a){if(!this.preventDefault)return new d.Event(a);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?y:x):this.type=a,this.timeStamp=d.now(),this[d.expando]=!0},d.Event.prototype={preventDefault:function(){this.isDefaultPrevented=y;var a=this.originalEvent;a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=y;var a=this.originalEvent;a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=y,this.stopPropagation()},isDefaultPrevented:x,isPropagationStopped:x,isImmediatePropagationStopped:x};var z=function(a){var b=a.relatedTarget;try{if(b&&b!==c&&!b.parentNode)return;while(b&&b!==this)b=b.parentNode;b!==this&&(a.type=a.data,d.event.handle.apply(this,arguments))}catch(e){}},A=function(a){a.type=a.data,d.event.handle.apply(this,arguments)};d.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){d.event.special[a]={setup:function(c){d.event.add(this,b,c&&c.selector?A:z,a)},teardown:function(a){d.event.remove(this,b,a&&a.selector?A:z)}}}),d.support.submitBubbles||(d.event.special.submit={setup:function(a,b){if(this.nodeName&&this.nodeName.toLowerCase()!=="form")d.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&d(b).closest("form").length&&E("submit",this,arguments)}),d.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&d(b).closest("form").length&&a.keyCode===13&&E("submit",this,arguments)});else return!1},teardown:function(a){d.event.remove(this,".specialSubmit")}});if(!d.support.changeBubbles){var B,C=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?d.map(a.options,function(a){return a.selected}).join("-"):"":a.nodeName.toLowerCase()==="select"&&(c=a.selectedIndex);return c},D=function D(a){var c=a.target,e,f;if(s.test(c.nodeName)&&!c.readOnly){e=d._data(c,"_change_data"),f=C(c),(a.type!=="focusout"||c.type!=="radio")&&d._data(c,"_change_data",f);if(e===b||f===e)return;if(e!=null||f)a.type="change",a.liveFired=b,d.event.trigger(a,arguments[1],c)}};d.event.special.change={filters:{focusout:D,beforedeactivate:D,click:function(a){var b=a.target,c=b.type;(c==="radio"||c==="checkbox"||b.nodeName.toLowerCase()==="select")&&D.call(this,a)},keydown:function(a){var b=a.target,c=b.type;(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&D.call(this,a)},beforeactivate:function(a){var b=a.target;d._data(b,"_change_data",C(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in B)d.event.add(this,c+".specialChange",B[c]);return s.test(this.nodeName)},teardown:function(a){d.event.remove(this,".specialChange");return s.test(this.nodeName)}},B=d.event.special.change.filters,B.focus=B.beforeactivate}c.addEventListener&&d.each({focus:"focusin",blur:"focusout"},function(a,b){function f(a){var c=d.event.fix(a);c.type=b,c.originalEvent={},d.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var e=0;d.event.special[b]={setup:function(){e++===0&&c.addEventListener(a,f,!0)},teardown:function(){--e===0&&c.removeEventListener(a,f,!0)}}}),d.each(["bind","one"],function(a,c){d.fn[c]=function(a,e,f){if(typeof a==="object"){for(var g in a)this[c](g,e,a[g],f);return this}if(d.isFunction(e)||e===!1)f=e,e=b;var h=c==="one"?d.proxy(f,function(a){d(this).unbind(a,h);return f.apply(this,arguments)}):f;if(a==="unload"&&c!=="one")this.one(a,e,f);else for(var i=0,j=this.length;i<j;i++)d.event.add(this[i],a,h,e);return this}}),d.fn.extend({unbind:function(a,b){if(typeof a!=="object"||a.preventDefault)for(var e=0,f=this.length;e<f;e++)d.event.remove(this[e],a,b);else for(var c in a)this.unbind(c,a[c]);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){d.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){var c=d.Event(a);c.preventDefault(),c.stopPropagation(),d.event.trigger(c,b,this[0]);return c.result}},toggle:function(a){var b=arguments,c=1;while(c<b.length)d.proxy(a,b[c++]);return this.click(d.proxy(a,function(e){var f=(d._data(this,"lastToggle"+a.guid)||0)%c;d._data(this,"lastToggle"+a.guid,f+1),e.preventDefault();return b[f].apply(this,arguments)||!1}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var F={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};d.each(["live","die"],function(a,c){d.fn[c]=function(a,e,f,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:d(this.context);if(typeof a==="object"&&!a.preventDefault){for(var o in a)n[c](o,e,a[o],m);return this}d.isFunction(e)&&(f=e,e=b),a=(a||"").split(" ");while((h=a[i++])!=null){j=r.exec(h),k="",j&&(k=j[0],h=h.replace(r,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,h==="focus"||h==="blur"?(a.push(F[h]+k),h=h+k):h=(F[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)d.event.add(n[p],"live."+H(h,m),{data:e,selector:m,handler:f,origType:h,origHandler:f,preType:l});else n.unbind("live."+H(h,m),f)}return this}}),d.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){d.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},d.attrFn&&(d.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!=="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,e,g){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!=="string")return e;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(f.call(n)==="[object Array]")if(u)if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&e.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&e.push(j[t]);else e.push.apply(e,n);else p(n,e);o&&(k(o,h,e,g),k.uniqueSort(e));return e};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!=="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(f){if(f===!0)continue}else g=o=!0}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b==="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1){}a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b==="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=u;typeof b==="string"&&!j.test(b)&&(b=b.toLowerCase(),d=b,g=t),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=u;typeof b==="string"&&!j.test(b)&&(b=b.toLowerCase(),d=b,g=t),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!=="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!=="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!=="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return"text"===c&&(b===c||b===null)},radio:function(a){return"radio"===a.type},checkbox:function(a){return"checkbox"===a.type},file:function(a){return"file"===a.type},password:function(a){return"password"===a.type},submit:function(a){return"submit"===a.type},image:function(a){return"image"===a.type},reset:function(a){return"reset"===a.type},button:function(a){return"button"===a.type||a.nodeName.toLowerCase()==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(f.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length==="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(a===b){g=!0;return 0}if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!=="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!=="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!=="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!=="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!=="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};d.find=k,d.expr=k.selectors,d.expr[":"]=d.expr.filters,d.unique=k.uniqueSort,d.text=k.getText,d.isXMLDoc=k.isXML,d.contains=k.contains}();var I=/Until$/,J=/^(?:parents|prevUntil|prevAll)/,K=/,/,L=/^.[^:#\[\.,]*$/,M=Array.prototype.slice,N=d.expr.match.POS,O={children:!0,contents:!0,next:!0,prev:!0};d.fn.extend({find:function(a){var b=this.pushStack("","find",a),c=0;for(var e=0,f=this.length;e<f;e++){c=b.length,d.find(a,this[e],b);if(e>0)for(var g=c;g<b.length;g++)for(var h=0;h<c;h++)if(b[h]===b[g]){b.splice(g--,1);break}}return b},has:function(a){var b=d(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(d.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(Q(this,a,!1),"not",a)},filter:function(a){return this.pushStack(Q(this,a,!0),"filter",a)},is:function(a){return!!a&&d.filter(a,this).length>0},closest:function(a,b){var c=[],e,f,g=this[0];if(d.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(e=0,f=a.length;e<f;e++)i=a[e],j[i]||(j[i]=d.expr.match.POS.test(i)?d(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:d(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=N.test(a)?d(a,b||this.context):null;for(e=0,f=this.length;e<f;e++){g=this[e];while(g){if(l?l.index(g)>-1:d.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b)break}}c=c.length>1?d.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a==="string")return d.inArray(this[0],a?d(a):this.parent().children());return d.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a==="string"?d(a,b):d.makeArray(a),e=d.merge(this.get(),c);return this.pushStack(P(c[0])||P(e[0])?e:d.unique(e))},andSelf:function(){return this.add(this.prevObject)}}),d.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return d.dir(a,"parentNode")},parentsUntil:function(a,b,c){return d.dir(a,"parentNode",c)},next:function(a){return d.nth(a,2,"nextSibling")},prev:function(a){return d.nth(a,2,"previousSibling")},nextAll:function(a){return d.dir(a,"nextSibling")},prevAll:function(a){return d.dir(a,"previousSibling")},nextUntil:function(a,b,c){return d.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return d.dir(a,"previousSibling",c)},siblings:function(a){return d.sibling(a.parentNode.firstChild,a)},children:function(a){return d.sibling(a.firstChild)},contents:function(a){return d.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:d.makeArray(a.childNodes)}},function(a,b){d.fn[a]=function(c,e){var f=d.map(this,b,c),g=M.call(arguments);I.test(a)||(e=c),e&&typeof e==="string"&&(f=d.filter(e,f)),f=this.length>1&&!O[a]?d.unique(f):f,(this.length>1||K.test(e))&&J.test(a)&&(f=f.reverse());return this.pushStack(f,a,g.join(","))}}),d.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?d.find.matchesSelector(b[0],a)?[b[0]]:[]:d.find.matches(a,b)},dir:function(a,c,e){var f=[],g=a[c];while(g&&g.nodeType!==9&&(e===b||g.nodeType!==1||!d(g).is(e)))g.nodeType===1&&f.push(g),g=g[c];return f},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var R=/ jQuery\d+="(?:\d+|null)"/g,S=/^\s+/,T=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,U=/<([\w:]+)/,V=/<tbody/i,W=/<|&#?\w+;/,X=/<(?:script|object|embed|option|style)/i,Y=/checked\s*(?:[^=]|=\s*.checked.)/i,Z={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};Z.optgroup=Z.option,Z.tbody=Z.tfoot=Z.colgroup=Z.caption=Z.thead,Z.th=Z.td,d.support.htmlSerialize||(Z._default=[1,"div<div>","</div>"]),d.fn.extend({text:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.text(a.call(this,b,c.text()))});if(typeof a!=="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return d.text(this)},wrapAll:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapAll(a.call(this,b))});if(this[0]){var b=d(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapInner(a.call(this,b))});return this.each(function(){var b=d(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){d(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=d(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,d(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,e;(e=this[c])!=null;c++)if(!a||d.filter(a,[e]).length)!b&&e.nodeType===1&&(d.cleanData(e.getElementsByTagName("*")),d.cleanData([e])),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&d.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return d.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(R,""):null;if(typeof a!=="string"||X.test(a)||!d.support.leadingWhitespace&&S.test(a)||Z[(U.exec(a)||["",""])[1].toLowerCase()])d.isFunction(a)?this.each(function(b){var c=d(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);else{a=a.replace(T,"<$1></$2>");try{for(var c=0,e=this.length;c<e;c++)this[c].nodeType===1&&(d.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(f){this.empty().append(a)}}return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(d.isFunction(a))return this.each(function(b){var c=d(this),e=c.html();c.replaceWith(a.call(this,b,e))});typeof a!=="string"&&(a=d(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;d(this).remove(),b?d(b).before(a):d(c).append(a)})}return this.length?this.pushStack(d(d.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,e){var f,g,h,i,j=a[0],k=[];if(!d.support.checkClone&&arguments.length===3&&typeof j==="string"&&Y.test(j))return this.each(function(){d(this).domManip(a,c,e,!0)});if(d.isFunction(j))return this.each(function(f){var g=d(this);a[0]=j.call(this,f,c?g.html():b),g.domManip(a,c,e)});if(this[0]){i=j&&j.parentNode,d.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?f={fragment:i}:f=d.buildFragment(a,this,k),h=f.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&d.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)e.call(c?$(this[l],g):this[l],f.cacheable||m>1&&l<n?d.clone(h,!0,!0):h)}k.length&&d.each(k,bc)}return this}}),d.buildFragment=function(a,b,e){var f,g,h,i=b&&b[0]?b[0].ownerDocument||b[0]:c;a.length===1&&typeof a[0]==="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!X.test(a[0])&&(d.support.checkClone||!Y.test(a[0]))&&(g=!0,h=d.fragments[a[0]],h&&(h!==1&&(f=h))),f||(f=i.createDocumentFragment(),d.clean(a,i,f,e)),g&&(d.fragments[a[0]]=h?f:1);return{fragment:f,cacheable:g}},d.fragments={},d.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){d.fn[a]=function(c){var e=[],f=d(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&f.length===1){f[b](this[0]);return this}for(var h=0,i=f.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();d(f[h])[b](j),e=e.concat(j)}return this.pushStack(e,a,f.selector)}}),d.extend({clone:function(a,b,c){var e=a.cloneNode(!0),f,g,h;if((!d.support.noCloneEvent||!d.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!d.isXMLDoc(a)){ba(a,e),f=bb(a),g=bb(e);for(h=0;f[h];++h)ba(f[h],g[h])}if(b){_(a,e);if(c){f=bb(a),g=bb(e);for(h=0;f[h];++h)_(f[h],g[h])}}return e},clean:function(a,b,e,f){b=b||c,typeof b.createElement==="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var g=[];for(var h=0,i;(i=a[h])!=null;h++){typeof i==="number"&&(i+="");if(!i)continue;if(typeof i!=="string"||W.test(i)){if(typeof i==="string"){i=i.replace(T,"<$1></$2>");var j=(U.exec(i)||["",""])[1].toLowerCase(),k=Z[j]||Z._default,l=k[0],m=b.createElement("div");m.innerHTML=k[1]+i+k[2];while(l--)m=m.lastChild;if(!d.support.tbody){var n=V.test(i),o=j==="table"&&!n?m.firstChild&&m.firstChild.childNodes:k[1]==="<table>"&&!n?m.childNodes:[];for(var p=o.length-1;p>=0;--p)d.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!d.support.leadingWhitespace&&S.test(i)&&m.insertBefore(b.createTextNode(S.exec(i)[0]),m.firstChild),i=m.childNodes}}else i=b.createTextNode(i);i.nodeType?g.push(i):g=d.merge(g,i)}if(e)for(h=0;g[h];h++)!f||!d.nodeName(g[h],"script")||g[h].type&&g[h].type.toLowerCase()!=="text/javascript"?(g[h].nodeType===1&&g.splice.apply(g,[h+1,0].concat(d.makeArray(g[h].getElementsByTagName("script")))),e.appendChild(g[h])):f.push(g[h].parentNode?g[h].parentNode.removeChild(g[h]):g[h]);return g},cleanData:function(a){var b,c,e=d.cache,f=d.expando,g=d.event.special,h=d.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&d.noData[j.nodeName.toLowerCase()])continue;c=j[d.expando];if(c){b=e[c]&&e[c][f];if(b&&b.events){for(var k in b.events)g[k]?d.event.remove(j,k):d.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[d.expando]:j.removeAttribute&&j.removeAttribute(d.expando),delete e[c]}}}});var bd=/alpha\([^)]*\)/i,be=/opacity=([^)]*)/,bf=/-([a-z])/ig,bg=/([A-Z]|^ms)/g,bh=/^-?\d+(?:px)?$/i,bi=/^-?\d/,bj={position:"absolute",visibility:"hidden",display:"block"},bk=["Left","Right"],bl=["Top","Bottom"],bm,bn,bo,bp=function(a,b){return b.toUpperCase()};d.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return d.access(this,a,c,!0,function(a,c,e){return e!==b?d.style(a,c,e):d.css(a,c)})},d.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bm(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0},cssProps:{"float":d.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,e,f){if(a&&a.nodeType!==3&&a.nodeType!==8&&a.style){var g,h=d.camelCase(c),i=a.style,j=d.cssHooks[h];c=d.cssProps[h]||h;if(e===b){if(j&&"get"in j&&(g=j.get(a,!1,f))!==b)return g;return i[c]}if(typeof e==="number"&&isNaN(e)||e==null)return;typeof e==="number"&&!d.cssNumber[h]&&(e+="px");if(!j||!("set"in j)||(e=j.set(a,e))!==b)try{i[c]=e}catch(k){}}},css:function(a,c,e){var f,g=d.camelCase(c),h=d.cssHooks[g];c=d.cssProps[g]||g;if(h&&"get"in h&&(f=h.get(a,!0,e))!==b)return f;if(bm)return bm(a,c,g)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bf,bp)}}),d.curCSS=d.css,d.each(["height","width"],function(a,b){d.cssHooks[b]={get:function(a,c,e){var f;if(c){a.offsetWidth!==0?f=bq(a,b,e):d.swap(a,bj,function(){f=bq(a,b,e)});if(f<=0){f=bm(a,b,b),f==="0px"&&bo&&(f=bo(a,b,b));if(f!=null)return f===""||f==="auto"?"0px":f}if(f<0||f==null){f=a.style[b];return f===""||f==="auto"?"0px":f}return typeof f==="string"?f:f+"px"}},set:function(a,b){if(!bh.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),d.support.opacity||(d.cssHooks.opacity={get:function(a,b){return be.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style;c.zoom=1;var e=d.isNaN(b)?"":"alpha(opacity="+b*100+")",f=c.filter||"";c.filter=bd.test(f)?f.replace(bd,e):c.filter+" "+e}}),d(function(){d.support.reliableMarginRight||(d.cssHooks.marginRight={get:function(a,b){var c;d.swap(a,{display:"inline-block"},function(){b?c=bm(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bn=function(a,c,e){var f,g,h;e=e.replace(bg,"-$1").toLowerCase();if(!(g=a.ownerDocument.defaultView))return b;if(h=g.getComputedStyle(a,null))f=h.getPropertyValue(e),f===""&&!d.contains(a.ownerDocument.documentElement,a)&&(f=d.style(a,e));return f}),c.documentElement.currentStyle&&(bo=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bh.test(d)&&bi.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bm=bn||bo,d.expr&&d.expr.filters&&(d.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!d.support.reliableHiddenOffsets&&(a.style.display||d.css(a,"display"))==="none"},d.expr.filters.visible=function(a){return!d.expr.filters.hidden(a)});var br=/%20/g,bs=/\[\]$/,bt=/\r?\n/g,bu=/#.*$/,bv=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bw=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bx=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,by=/^(?:GET|HEAD)$/,bz=/^\/\//,bA=/\?/,bB=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bC=/^(?:select|textarea)/i,bD=/\s+/,bE=/([?&])_=[^&]*/,bF=/(^|\-)([a-z])/g,bG=function(a,b,c){return b+c.toUpperCase()},bH=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bI=d.fn.load,bJ={},bK={},bL,bM;try{bL=c.location.href}catch(bN){bL=c.createElement("a"),bL.href="",bL=bL.href}bM=bH.exec(bL.toLowerCase())||[],d.fn.extend({load:function(a,c,e){if(typeof a!=="string"&&bI)return bI.apply(this,arguments);if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var g=a.slice(f,a.length);a=a.slice(0,f)}var h="GET";c&&(d.isFunction(c)?(e=c,c=b):typeof c==="object"&&(c=d.param(c,d.ajaxSettings.traditional),h="POST"));var i=this;d.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?d("<div>").append(c.replace(bB,"")).find(g):c)),e&&i.each(e,[c,b,a])}});return this},serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?d.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bC.test(this.nodeName)||bw.test(this.type))}).map(function(a,b){var c=d(this).val();return c==null?null:d.isArray(c)?d.map(c,function(a,c){return{name:b.name,value:a.replace(bt,"\r\n")}}):{name:b.name,value:c.replace(bt,"\r\n")}}).get()}}),d.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){d.fn[b]=function(a){return this.bind(b,a)}}),d.each(["get","post"],function(a,c){d[c]=function(a,e,f,g){d.isFunction(e)&&(g=g||f,f=e,e=b);return d.ajax({type:c,url:a,data:e,success:f,dataType:g})}}),d.extend({getScript:function(a,c){return d.get(a,b,c,"script")},getJSON:function(a,b,c){return d.get(a,b,c,"json")},ajaxSetup:function(a,b){b?d.extend(!0,a,d.ajaxSettings,b):(b=a,a=d.extend(!0,d.ajaxSettings,b));for(var c in {context:1,url:1})c in b?a[c]=b[c]:c in d.ajaxSettings&&(a[c]=d.ajaxSettings[c]);return a},ajaxSettings:{url:bL,isLocal:bx.test(bM[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML}},ajaxPrefilter:bO(bJ),ajaxTransport:bO(bK),ajax:function(a,c){function v(a,c,l,n){if(r!==2){r=2,p&&clearTimeout(p),o=b,m=n||"",u.readyState=a?4:0;var q,t,v,w=l?bR(e,u,l):b,x,y;if(a>=200&&a<300||a===304){if(e.ifModified){if(x=u.getResponseHeader("Last-Modified"))d.lastModified[k]=x;if(y=u.getResponseHeader("Etag"))d.etag[k]=y}if(a===304)c="notmodified",q=!0;else try{t=bS(e,w),c="success",q=!0}catch(z){c="parsererror",v=z}}else{v=c;if(!c||a)c="error",a<0&&(a=0)}u.status=a,u.statusText=c,q?h.resolveWith(f,[t,c,u]):h.rejectWith(f,[u,c,v]),u.statusCode(j),j=b,s&&g.trigger("ajax"+(q?"Success":"Error"),[u,e,q?t:v]),i.resolveWith(f,[u,c]),s&&(g.trigger("ajaxComplete",[u,e]),--d.active||d.event.trigger("ajaxStop"))}}typeof a==="object"&&(c=a,a=b),c=c||{};var e=d.ajaxSetup({},c),f=e.context||e,g=f!==e&&(f.nodeType||f instanceof d)?d(f):d.event,h=d.Deferred(),i=d._Deferred(),j=e.statusCode||{},k,l={},m,n,o,p,q,r=0,s,t,u={readyState:0,setRequestHeader:function(a,b){r||(l[a.toLowerCase().replace(bF,bG)]=b);return this},getAllResponseHeaders:function(){return r===2?m:null},getResponseHeader:function(a){var c;if(r===2){if(!n){n={};while(c=bv.exec(m))n[c[1].toLowerCase()]=c[2]}c=n[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){r||(e.mimeType=a);return this},abort:function(a){a=a||"abort",o&&o.abort(a),v(0,a);return this}};h.promise(u),u.success=u.done,u.error=u.fail,u.complete=i.done,u.statusCode=function(a){if(a){var b;if(r<2)for(b in a)j[b]=[j[b],a[b]];else b=a[u.status],u.then(b,b)}return this},e.url=((a||e.url)+"").replace(bu,"").replace(bz,bM[1]+"//"),e.dataTypes=d.trim(e.dataType||"*").toLowerCase().split(bD),e.crossDomain==null&&(q=bH.exec(e.url.toLowerCase()),e.crossDomain=q&&(q[1]!=bM[1]||q[2]!=bM[2]||(q[3]||(q[1]==="http:"?80:443))!=(bM[3]||(bM[1]==="http:"?80:443)))),e.data&&e.processData&&typeof e.data!=="string"&&(e.data=d.param(e.data,e.traditional)),bP(bJ,e,c,u);if(r===2)return!1;s=e.global,e.type=e.type.toUpperCase(),e.hasContent=!by.test(e.type),s&&d.active++===0&&d.event.trigger("ajaxStart");if(!e.hasContent){e.data&&(e.url+=(bA.test(e.url)?"&":"?")+e.data),k=e.url;if(e.cache===!1){var w=d.now(),x=e.url.replace(bE,"$1_="+w);e.url=x+(x===e.url?(bA.test(e.url)?"&":"?")+"_="+w:"")}}if(e.data&&e.hasContent&&e.contentType!==!1||c.contentType)l["Content-Type"]=e.contentType;e.ifModified&&(k=k||e.url,d.lastModified[k]&&(l["If-Modified-Since"]=d.lastModified[k]),d.etag[k]&&(l["If-None-Match"]=d.etag[k])),l.Accept=e.dataTypes[0]&&e.accepts[e.dataTypes[0]]?e.accepts[e.dataTypes[0]]+(e.dataTypes[0]!=="*"?", */*; q=0.01":""):e.accepts["*"];for(t in e.headers)u.setRequestHeader(t,e.headers[t]);if(e.beforeSend&&(e.beforeSend.call(f,u,e)===!1||r===2)){u.abort();return!1}for(t in {success:1,error:1,complete:1})u[t](e[t]);o=bP(bK,e,c,u);if(o){u.readyState=1,s&&g.trigger("ajaxSend",[u,e]),e.async&&e.timeout>0&&(p=setTimeout(function(){u.abort("timeout")},e.timeout));try{r=1,o.send(l,v)}catch(y){status<2?v(-1,y):d.error(y)}}else v(-1,"No Transport");return u},param:function(a,c){var e=[],f=function(a,b){b=d.isFunction(b)?b():b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=d.ajaxSettings.traditional);if(d.isArray(a)||a.jquery&&!d.isPlainObject(a))d.each(a,function(){f(this.name,this.value)});else for(var g in a)bQ(g,a[g],c,f);return e.join("&").replace(br,"+")}}),d.extend({active:0,lastModified:{},etag:{}});var bT=d.now(),bU=/(\=)\?(&|$)|\?\?/i;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return d.expando+"_"+bT++}}),d.ajaxPrefilter("json jsonp",function(b,c,e){var f=typeof b.data==="string";if(b.dataTypes[0]==="jsonp"||c.jsonpCallback||c.jsonp!=null||b.jsonp!==!1&&(bU.test(b.url)||f&&bU.test(b.data))){var g,h=b.jsonpCallback=d.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2",m=function(){a[h]=i,g&&d.isFunction(i)&&a[h](g[0])};b.jsonp!==!1&&(j=j.replace(bU,l),b.url===j&&(f&&(k=k.replace(bU,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},e.then(m,m),b.converters["script json"]=function(){g||d.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){d.globalEval(a);return a}}}),d.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),d.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var bV=d.now(),bW,bX;d.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&bZ()||b$()}:bZ,bX=d.ajaxSettings.xhr(),d.support.ajax=!!bX,d.support.cors=bX&&"withCredentials"in bX,bX=b,d.support.ajax&&d.ajaxTransport(function(a){if(!a.crossDomain||d.support.cors){var c;return{send:function(e,f){var g=a.xhr(),h,i;a.username?g.open(a.type,a.url,a.async,a.username,a.password):g.open(a.type,a.url,a.async);if(a.xhrFields)for(i in a.xhrFields)g[i]=a.xhrFields[i];a.mimeType&&g.overrideMimeType&&g.overrideMimeType(a.mimeType),!a.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(i in e)g.setRequestHeader(i,e[i])}catch(j){}g.send(a.hasContent&&a.data||null),c=function(e,i){var j,k,l,m,n;try{if(c&&(i||g.readyState===4)){c=b,h&&(g.onreadystatechange=d.noop,delete bW[h]);if(i)g.readyState!==4&&g.abort();else{j=g.status,l=g.getAllResponseHeaders(),m={},n=g.responseXML,n&&n.documentElement&&(m.xml=n),m.text=g.responseText;try{k=g.statusText}catch(o){k=""}j||!a.isLocal||a.crossDomain?j===1223&&(j=204):j=m.text?200:404}}}catch(p){i||f(-1,p)}m&&f(j,k,m,l)},a.async&&g.readyState!==4?(bW||(bW={},bY()),h=bV++,g.onreadystatechange=bW[h]=c):c()},abort:function(){c&&c(0,1)}}}});var b_={},ca=/^(?:toggle|show|hide)$/,cb=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cc,cd=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];d.fn.extend({show:function(a,b,c){var e,f;if(a||a===0)return this.animate(ce("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)e=this[g],f=e.style.display,!d._data(e,"olddisplay")&&f==="none"&&(f=e.style.display=""),f===""&&d.css(e,"display")==="none"&&d._data(e,"olddisplay",cf(e.nodeName));for(g=0;g<h;g++){e=this[g],f=e.style.display;if(f===""||f==="none")e.style.display=d._data(e,"olddisplay")||""}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ce("hide",3),a,b,c);for(var e=0,f=this.length;e<f;e++){var g=d.css(this[e],"display");g!=="none"&&!d._data(this[e],"olddisplay")&&d._data(this[e],"olddisplay",g)}for(e=0;e<f;e++)this[e].style.display="none";return this},_toggle:d.fn.toggle,toggle:function(a,b,c){var e=typeof a==="boolean";d.isFunction(a)&&d.isFunction(b)?this._toggle.apply(this,arguments):a==null||e?this.each(function(){var b=e?a:d(this).is(":hidden");d(this)[b?"show":"hide"]()}):this.animate(ce("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,e){var f=d.speed(b,c,e);if(d.isEmptyObject(a))return this.each(f.complete);return this[f.queue===!1?"each":"queue"](function(){var b=d.extend({},f),c,e=this.nodeType===1,g=e&&d(this).is(":hidden"),h=this;for(c in a){var i=d.camelCase(c);c!==i&&(a[i]=a[c],delete a[c],c=i);if(a[c]==="hide"&&g||a[c]==="show"&&!g)return b.complete.call(this);if(e&&(c==="height"||c==="width")){b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY];if(d.css(this,"display")==="inline"&&d.css(this,"float")==="none")if(d.support.inlineBlockNeedsLayout){var j=cf(this.nodeName);j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)}else this.style.display="inline-block"}d.isArray(a[c])&&((b.specialEasing=b.specialEasing||{})[c]=a[c][1],a[c]=a[c][0])}b.overflow!=null&&(this.style.overflow="hidden"),b.curAnim=d.extend({},a),d.each(a,function(c,e){var f=new d.fx(h,b,c);if(ca.test(e))f[e==="toggle"?g?"show":"hide":e](a);else{var i=cb.exec(e),j=f.cur();if(i){var k=parseFloat(i[2]),l=i[3]||(d.cssNumber[c]?"":"px");l!=="px"&&(d.style(h,c,(k||1)+l),j=(k||1)/f.cur()*j,d.style(h,c,j+l)),i[1]&&(k=(i[1]==="-="?-1:1)*k+j),f.custom(j,k,l)}else f.custom(j,e,"")}});return!0})},stop:function(a,b){var c=d.timers;a&&this.queue([]),this.each(function(){for(var a=c.length-1;a>=0;a--)c[a].elem===this&&(b&&c[a](!0),c.splice(a,1))}),b||this.dequeue();return this}}),d.each({slideDown:ce("show",1),slideUp:ce("hide",1),slideToggle:ce("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){d.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),d.extend({speed:function(a,b,c){var e=a&&typeof a==="object"?d.extend({},a):{complete:c||!c&&b||d.isFunction(a)&&a,duration:a,easing:c&&b||b&&!d.isFunction(b)&&b};e.duration=d.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in d.fx.speeds?d.fx.speeds[e.duration]:d.fx.speeds._default,e.old=e.complete,e.complete=function(){e.queue!==!1&&d(this).dequeue(),d.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig||(b.orig={})}}),d.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(d.fx.step[this.prop]||d.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=d.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function g(a){return e.step(a)}var e=this,f=d.fx;this.startTime=d.now(),this.start=a,this.end=b,this.unit=c||this.unit||(d.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,g.elem=this.elem,g()&&d.timers.push(g)&&!cc&&(cc=setInterval(f.tick,f.interval))},show:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),d(this.elem).show()},hide:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=d.now(),c=!0;if(a||b>=this.options.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),this.options.curAnim[this.prop]=!0;for(var e in this.options.curAnim)this.options.curAnim[e]!==!0&&(c=!1);if(c){if(this.options.overflow!=null&&!d.support.shrinkWrapBlocks){var f=this.elem,g=this.options;d.each(["","X","Y"],function(a,b){f.style["overflow"+b]=g.overflow[a]})}this.options.hide&&d(this.elem).hide();if(this.options.hide||this.options.show)for(var h in this.options.curAnim)d.style(this.elem,h,this.options.orig[h]);this.options.complete.call(this.elem)}return!1}var i=b-this.startTime;this.state=i/this.options.duration;var j=this.options.specialEasing&&this.options.specialEasing[this.prop],k=this.options.easing||(d.easing.swing?"swing":"linear");this.pos=d.easing[j||k](this.state,i,0,1,this.options.duration),this.now=this.start+(this.end-this.start)*this.pos,this.update();return!0}},d.extend(d.fx,{tick:function(){var a=d.timers;for(var b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||d.fx.stop()},interval:13,stop:function(){clearInterval(cc),cc=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){d.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),d.expr&&d.expr.filters&&(d.expr.filters.animated=function(a){return d.grep(d.timers,function(b){return a===b.elem}).length});var cg=/^t(?:able|d|h)$/i,ch=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?d.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){d.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return d.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(e){}var f=b.ownerDocument,g=f.documentElement;if(!c||!d.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=f.body,i=ci(f),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||d.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||d.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:d.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){d.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return d.offset.bodyOffset(b);d.offset.initialize();var c,e=b.offsetParent,f=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(d.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===e&&(l+=b.offsetTop,m+=b.offsetLeft,d.offset.doesNotAddBorder&&(!d.offset.doesAddBorderForTableAndCells||!cg.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),f=e,e=b.offsetParent),d.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;d.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},d.offset={initialize:function(){var a=c.body,b=c.createElement("div"),e,f,g,h,i=parseFloat(d.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";d.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),e=b.firstChild,f=e.firstChild,h=e.nextSibling.firstChild.firstChild,this.doesNotAddBorder=f.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,f.style.position="fixed",f.style.top="20px",this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15,f.style.position=f.style.top="",e.style.overflow="hidden",e.style.position="relative",this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),d.offset.initialize=d.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;d.offset.initialize(),d.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(d.css(a,"marginTop"))||0,c+=parseFloat(d.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var e=d.css(a,"position");e==="static"&&(a.style.position="relative");var f=d(a),g=f.offset(),h=d.css(a,"top"),i=d.css(a,"left"),j=(e==="absolute"||e==="fixed")&&d.inArray("auto",[h,i])>-1,k={},l={},m,n;j&&(l=f.position()),m=j?l.top:parseInt(h,10)||0,n=j?l.left:parseInt(i,10)||0,d.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):f.css(k)}},d.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),e=ch.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(d.css(a,"marginTop"))||0,c.left-=parseFloat(d.css(a,"marginLeft"))||0,e.top+=parseFloat(d.css(b[0],"borderTopWidth"))||0,e.left+=parseFloat(d.css(b[0],"borderLeftWidth"))||0;return{top:c.top-e.top,left:c.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&(!ch.test(a.nodeName)&&d.css(a,"position")==="static"))a=a.offsetParent;return a})}}),d.each(["Left","Top"],function(a,c){var e="scroll"+c;d.fn[e]=function(c){var f=this[0],g;if(!f)return null;if(c!==b)return this.each(function(){g=ci(this),g?g.scrollTo(a?d(g).scrollLeft():c,a?c:d(g).scrollTop()):this[e]=c});g=ci(f);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:d.support.boxModel&&g.document.documentElement[e]||g.document.body[e]:f[e]}}),d.each(["Height","Width"],function(a,c){var e=c.toLowerCase();d.fn["inner"+c]=function(){return this[0]?parseFloat(d.css(this[0],e,"padding")):null},d.fn["outer"+c]=function(a){return this[0]?parseFloat(d.css(this[0],e,a?"margin":"border")):null},d.fn[e]=function(a){var f=this[0];if(!f)return a==null?null:this;if(d.isFunction(a))return this.each(function(b){var c=d(this);c[e](a.call(this,b,c[e]()))});if(d.isWindow(f)){var g=f.document.documentElement["client"+c];return f.document.compatMode==="CSS1Compat"&&g||f.document.body["client"+c]||g}if(f.nodeType===9)return Math.max(f.documentElement["client"+c],f.body["scroll"+c],f.documentElement["scroll"+c],f.body["offset"+c],f.documentElement["offset"+c]);if(a===b){var h=d.css(f,e),i=parseFloat(h);return d.isNaN(i)?h:i}return this.css(e,typeof a==="string"?a:a+"px")}}),a.jQuery=a.$=d})(window);
\ No newline at end of file
diff --git a/aos/network/www_defaults/_seasocks.css b/aos/network/www_defaults/_seasocks.css
new file mode 100644
index 0000000..03a7287
--- /dev/null
+++ b/aos/network/www_defaults/_seasocks.css
@@ -0,0 +1,22 @@
+body {
+ font-family: segoe ui, tahoma, arial, sans-serif;
+ font-size: 12px;
+ color: #ffffff;
+ background-color: #333333;
+ margin: 0;
+}
+
+a {
+ color: #ffff00;
+}
+
+table {
+ border-collapse: collapse;
+ width: 100%;
+ text-align: center;
+}
+
+.template {
+ display: none;
+}
+
diff --git a/aos/network/www_defaults/_stats.html b/aos/network/www_defaults/_stats.html
new file mode 100644
index 0000000..d34e932
--- /dev/null
+++ b/aos/network/www_defaults/_stats.html
@@ -0,0 +1,60 @@
+<html DOCTYPE=html>
+<head>
+ <title>SeaSocks Stats</title>
+ <link href="/_seasocks.css" rel="stylesheet">
+ <script src="/_jquery.min.js" type="text/javascript"></script>
+ <script>
+ function clear() {
+ $('#cx tbody tr:visible').remove();
+ }
+ function connection(stats) {
+ c = $('#cx .template').clone().removeClass('template').appendTo('#cx');
+ for (stat in stats) {
+ c.find('.' + stat).text(stats[stat]);
+ }
+ }
+ function refresh() {
+ var stats = new XMLHttpRequest();
+ stats.open("GET", "/_livestats.js", false);
+ stats.send(null);
+ eval(stats.responseText);
+ }
+ $(function() {
+ setInterval(refresh, 1000);
+ refresh();
+ });
+ </script>
+</head>
+<body><h1>SeaSocks Stats</h1></body>
+
+<h2>Connections</h2>
+<table id="cx">
+ <thead>
+ <tr>
+ <th>Connection time</th>
+ <th>Fd</th>
+ <th>Addr</th>
+ <th>URI</th>
+ <th>Username</th>
+ <th>Pending read</th>
+ <th>Bytes read</th>
+ <th>Pending send</th>
+ <th>Bytes sent</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="template">
+ <td class="since"></td>
+ <td class="fd"></td>
+ <td class="addr"></td>
+ <td class="uri"></td>
+ <td class="user"></td>
+ <td class="input"></td>
+ <td class="read"></td>
+ <td class="output"></td>
+ <td class="written"></td>
+ </tr>
+ </tbody>
+</table>
+
+</body></html>
diff --git a/aos/network/www_defaults/favicon.ico b/aos/network/www_defaults/favicon.ico
new file mode 100644
index 0000000..30a95b9
--- /dev/null
+++ b/aos/network/www_defaults/favicon.ico
Binary files differ
diff --git a/aos/testing/BUILD b/aos/testing/BUILD
index 99a3569..9aa058d 100644
--- a/aos/testing/BUILD
+++ b/aos/testing/BUILD
@@ -24,7 +24,7 @@
visibility = ["//visibility:public"],
deps = [
":googletest",
- "//aos:once",
+ "@com_google_absl//absl/base",
"//aos/logging:implementations",
"//aos/mutex",
],
diff --git a/aos/testing/test_logging.cc b/aos/testing/test_logging.cc
index 1799cda..3d97c05 100644
--- a/aos/testing/test_logging.cc
+++ b/aos/testing/test_logging.cc
@@ -8,7 +8,7 @@
#include "aos/logging/implementations.h"
#include "aos/mutex/mutex.h"
-#include "aos/once.h"
+#include "absl/base/call_once.h"
using ::aos::logging::LogMessage;
@@ -39,8 +39,9 @@
// This class has to be a singleton so that everybody can get access to the
// same instance to read out the messages etc.
static TestLogImplementation *GetInstance() {
- static Once<TestLogImplementation> once(CreateInstance);
- return once.Get();
+ static absl::once_flag once;
+ absl::call_once(once, CreateInstance);
+ return instance;
}
// Clears out all of the messages already recorded.
@@ -72,6 +73,7 @@
void PrintMessagesAsTheyComeIn() { print_as_messages_come_in_ = true; }
private:
+ static TestLogImplementation *instance;
TestLogImplementation() {}
~TestLogImplementation() {
if (output_file_ != stdout) {
@@ -79,8 +81,8 @@
}
}
- static TestLogImplementation *CreateInstance() {
- return new TestLogImplementation();
+ static void CreateInstance() {
+ instance = new TestLogImplementation();
}
virtual void HandleMessage(const LogMessage &message) override {
@@ -104,6 +106,8 @@
static thread_local ::aos::monotonic_clock::time_point monotonic_now_;
};
+TestLogImplementation *TestLogImplementation::instance;
+
thread_local bool TestLogImplementation::mock_time_ = false;
thread_local ::aos::monotonic_clock::time_point
TestLogImplementation::monotonic_now_ = ::aos::monotonic_clock::min_time;
@@ -151,12 +155,12 @@
return nullptr;
}
-Once<void> enable_test_logging_once(DoEnableTestLogging);
+static absl::once_flag enable_test_logging_once;
} // namespace
void EnableTestLogging() {
- enable_test_logging_once.Get();
+ absl::call_once(enable_test_logging_once, DoEnableTestLogging);
}
void SetLogFileName(const char* filename) {
diff --git a/build_tests/BUILD b/build_tests/BUILD
index a4c5829..0e3b8f7 100644
--- a/build_tests/BUILD
+++ b/build_tests/BUILD
@@ -115,3 +115,12 @@
srcs = ["python_fbs.py"],
deps = [":test_python_fbs"],
)
+
+py_test(
+ name = "python3_opencv",
+ srcs = ["python_opencv.py"],
+ default_python_version = "PY3",
+ main = "python_opencv.py",
+ srcs_version = "PY2AND3",
+ deps = ["@opencv_contrib_nonfree_amd64//:python_opencv"],
+)
diff --git a/build_tests/python_opencv.py b/build_tests/python_opencv.py
new file mode 100644
index 0000000..c353c79
--- /dev/null
+++ b/build_tests/python_opencv.py
@@ -0,0 +1,6 @@
+#!/usr/bin/python3
+
+import cv2
+
+if __name__ == '__main__':
+ cv2.xfeatures2d.SIFT_create()
diff --git a/debian/opencv_python.BUILD b/debian/opencv_python.BUILD
new file mode 100644
index 0000000..5afa180
--- /dev/null
+++ b/debian/opencv_python.BUILD
@@ -0,0 +1,10 @@
+py_library(
+ name = "python_opencv",
+ srcs = glob(["**/*.py"]),
+ data = glob(
+ include = ["**/*"],
+ exclude = ["**/*.py"],
+ ),
+ imports = ["."],
+ visibility = ["//visibility:public"],
+)
diff --git a/debian/python.BUILD b/debian/python.BUILD
index 5e5d810..6f9150d 100644
--- a/debian/python.BUILD
+++ b/debian/python.BUILD
@@ -38,6 +38,7 @@
name = "python2.7_lib",
srcs = [
"usr/lib/x86_64-linux-gnu/libpython2.7.so",
+ "usr/lib/x86_64-linux-gnu/libpython2.7.so.1.0",
],
hdrs = glob([
"usr/include/**/*.h",
diff --git a/package.json b/package.json
index 0b20391..a627a3d 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"devDependencies": {
"@bazel/bazel": "^0.19.1",
"@bazel/typescript": "0.21.0",
+ "@types/flatbuffers": "latest",
"typescript": "^3.1.6"
}
}
diff --git a/third_party/flatbuffers/BUILD b/third_party/flatbuffers/BUILD
index 492f158..05553e1 100644
--- a/third_party/flatbuffers/BUILD
+++ b/third_party/flatbuffers/BUILD
@@ -237,3 +237,8 @@
srcs = glob(["python/flatbuffers/*.py"]),
imports = ["python/"],
)
+
+filegroup(
+ name = "flatjs",
+ srcs = ["js/flatbuffers.js"],
+)
diff --git a/third_party/flatbuffers/build_defs.bzl b/third_party/flatbuffers/build_defs.bzl
index da61852..2ddc65b 100644
--- a/third_party/flatbuffers/build_defs.bzl
+++ b/third_party/flatbuffers/build_defs.bzl
@@ -4,6 +4,7 @@
"""
Rules for building C++ flatbuffers with Bazel.
"""
+load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
flatc_path = "@com_github_google_flatbuffers//:flatc"
@@ -26,6 +27,14 @@
"--gen-name-strings",
]
+DEFAULT_FLATC_TS_ARGS = [
+ "--gen-all",
+ "--no-fb-import",
+ "--no-ts-reexport",
+ "--reflect-names",
+ "--reflect-types",
+]
+
def flatbuffer_library_public(
name,
srcs,
@@ -293,3 +302,40 @@
imports = ["."],
deps = ["@com_github_google_flatbuffers//:flatpy"],
)
+
+def flatbuffer_ts_library(
+ name,
+ srcs,
+ compatible_with = None,
+ includes = [],
+ include_paths = DEFAULT_INCLUDE_PATHS,
+ flatc_args = DEFAULT_FLATC_TS_ARGS,
+ visibility = None,
+ srcs_filegroup_visibility = None):
+ """Generates a ts_library rule for a given flatbuffer definition.
+
+ Args:
+ name: Name of the generated ts_library rule.
+ srcs: Source .fbs file(s).
+ """
+ srcs_lib = "%s_srcs" % (name)
+ outs = ["%s_generated.ts" % (s.replace(".fbs", "").split("/")[-1]) for s in srcs]
+ flatbuffer_library_public(
+ name = srcs_lib,
+ srcs = srcs,
+ outs = outs,
+ language_flag = "--ts",
+ includes = includes,
+ include_paths = include_paths,
+ flatc_args = flatc_args,
+ compatible_with = compatible_with,
+ )
+ ts_library(
+ name = name,
+ srcs = outs,
+ visibility = visibility,
+ compatible_with = compatible_with,
+ deps = [
+ "@npm//@types",
+ ],
+ )
diff --git a/tools/ci/clean-disk.sh b/tools/ci/clean-disk.sh
index 643ce07..e86d5a3 100755
--- a/tools/ci/clean-disk.sh
+++ b/tools/ci/clean-disk.sh
@@ -5,7 +5,7 @@
#Set default for disk utilisation
DMAX=${1:-80%}
#Retrieve disk usages in percentage
-DSIZE=$(df -hlP ~/jenkins | sed 1d | awk '{print $5}')
+DSIZE=$(df -hlP /home/jenkins | sed 1d | awk '{print $5}')
if [[ $DSIZE>$DMAX ]]; then
echo $NODE_NAME": Disk over "$DMAX" Clean up needed on node."
diff --git a/y2020/control_loops/superstructure/BUILD b/y2020/control_loops/superstructure/BUILD
index 533a369..88d574d 100644
--- a/y2020/control_loops/superstructure/BUILD
+++ b/y2020/control_loops/superstructure/BUILD
@@ -76,3 +76,29 @@
"//aos/events:shm_event_loop",
],
)
+
+cc_test(
+ name = "superstructure_lib_test",
+ srcs = [
+ "superstructure_lib_test.cc",
+ ],
+ data = [
+ "//y2020:config.json",
+ ],
+ deps = [
+ ":superstructure_goal_fbs",
+ ":superstructure_lib",
+ ":superstructure_output_fbs",
+ ":superstructure_position_fbs",
+ ":superstructure_status_fbs",
+ "//aos:math",
+ "//aos/controls:control_loop_test",
+ "//aos/testing:googletest",
+ "//aos/time",
+ "//frc971/control_loops:capped_test_plant",
+ "//frc971/control_loops:position_sensor_sim",
+ "//frc971/control_loops:team_number_test_environment",
+ "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
+ "//y2020/control_loops/superstructure/hood:hood_plants",
+ ],
+)
diff --git a/y2020/control_loops/superstructure/superstructure.cc b/y2020/control_loops/superstructure/superstructure.cc
index a3978a4..91a7586 100644
--- a/y2020/control_loops/superstructure/superstructure.cc
+++ b/y2020/control_loops/superstructure/superstructure.cc
@@ -12,32 +12,50 @@
Superstructure::Superstructure(::aos::EventLoop *event_loop,
const ::std::string &name)
: aos::controls::ControlLoop<Goal, Position, Status, Output>(event_loop,
- name) {
- event_loop->SetRuntimeRealtimePriority(30);
+ name),
+ hood_(constants::GetValues().hood) {
+ event_loop->SetRuntimeRealtimePriority(30);
}
-void Superstructure::RunIteration(const Goal * /*unsafe_goal*/,
- const Position * /*position*/,
+void Superstructure::RunIteration(const Goal *unsafe_goal,
+ const Position *position,
aos::Sender<Output>::Builder *output,
aos::Sender<Status>::Builder *status) {
if (WasReset()) {
AOS_LOG(ERROR, "WPILib reset, restarting\n");
+ hood_.Reset();
}
+ OutputT output_struct;
+
+ flatbuffers::Offset<AbsoluteEncoderProfiledJointStatus> hood_status_offset =
+ hood_.Iterate(unsafe_goal != nullptr ? unsafe_goal->hood() : nullptr,
+ position->hood(),
+ output != nullptr ? &(output_struct.hood_voltage) : nullptr,
+ status->fbb());
+
+ bool zeroed;
+ bool estopped;
+
+ const AbsoluteEncoderProfiledJointStatus *hood_status =
+ GetMutableTemporaryPointer(*status->fbb(), hood_status_offset);
+ zeroed = hood_status->zeroed();
+ estopped = hood_status->estopped();
if (output != nullptr) {
- OutputT output_struct;
output->Send(Output::Pack(*output->fbb(), &output_struct));
}
Status::Builder status_builder = status->MakeBuilder<Status>();
- status_builder.add_zeroed(true);
- status_builder.add_estopped(false);
+ status_builder.add_zeroed(zeroed);
+ status_builder.add_estopped(estopped);
+
+ status_builder.add_hood(hood_status_offset);
status->Send(status_builder.Finish());
}
+} // namespace superstructure
} // namespace control_loops
} // namespace y2020
-} // namespace y2020
diff --git a/y2020/control_loops/superstructure/superstructure.h b/y2020/control_loops/superstructure/superstructure.h
index 9aaca8e..dbc1ccd 100644
--- a/y2020/control_loops/superstructure/superstructure.h
+++ b/y2020/control_loops/superstructure/superstructure.h
@@ -19,12 +19,21 @@
explicit Superstructure(::aos::EventLoop *event_loop,
const ::std::string &name = "/superstructure");
+ using AbsoluteEncoderSubsystem =
+ ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystem<
+ ::frc971::zeroing::AbsoluteEncoderZeroingEstimator,
+ ::frc971::control_loops::AbsoluteEncoderProfiledJointStatus>;
+
+ const AbsoluteEncoderSubsystem &hood() const { return hood_; }
+
protected:
virtual void RunIteration(const Goal *unsafe_goal, const Position *position,
aos::Sender<Output>::Builder *output,
aos::Sender<Status>::Builder *status) override;
private:
+ AbsoluteEncoderSubsystem hood_;
+
DISALLOW_COPY_AND_ASSIGN(Superstructure);
};
diff --git a/y2020/control_loops/superstructure/superstructure_lib_test.cc b/y2020/control_loops/superstructure/superstructure_lib_test.cc
new file mode 100644
index 0000000..468e0c9
--- /dev/null
+++ b/y2020/control_loops/superstructure/superstructure_lib_test.cc
@@ -0,0 +1,349 @@
+#include <unistd.h>
+
+#include <chrono>
+#include <memory>
+
+#include "aos/controls/control_loop_test.h"
+#include "frc971/control_loops/capped_test_plant.h"
+#include "frc971/control_loops/position_sensor_sim.h"
+#include "frc971/control_loops/team_number_test_environment.h"
+#include "gtest/gtest.h"
+#include "y2020/constants.h"
+#include "y2020/control_loops/superstructure/hood/hood_plant.h"
+#include "y2020/control_loops/superstructure/superstructure.h"
+
+namespace y2020 {
+namespace control_loops {
+namespace superstructure {
+namespace testing {
+
+namespace {
+constexpr double kNoiseScalar = 0.01;
+} // namespace
+
+namespace chrono = ::std::chrono;
+using ::aos::monotonic_clock;
+using ::frc971::CreateProfileParameters;
+using ::frc971::control_loops::CappedTestPlant;
+using ::frc971::control_loops::
+ CreateStaticZeroingSingleDOFProfiledSubsystemGoal;
+using ::frc971::control_loops::PositionSensorSimulator;
+using ::frc971::control_loops::StaticZeroingSingleDOFProfiledSubsystemGoal;
+typedef Superstructure::AbsoluteEncoderSubsystem AbsoluteEncoderSubsystem;
+
+// Class which simulates the superstructure and sends out queue messages with
+// the position.
+class SuperstructureSimulation {
+ public:
+ SuperstructureSimulation(::aos::EventLoop *event_loop, chrono::nanoseconds dt)
+ : event_loop_(event_loop),
+ dt_(dt),
+ superstructure_position_sender_(
+ event_loop_->MakeSender<Position>("/superstructure")),
+ superstructure_status_fetcher_(
+ event_loop_->MakeFetcher<Status>("/superstructure")),
+ superstructure_output_fetcher_(
+ event_loop_->MakeFetcher<Output>("/superstructure")),
+
+ hood_plant_(new CappedTestPlant(hood::MakeHoodPlant())),
+ hood_encoder_(constants::GetValues()
+ .hood.zeroing_constants.one_revolution_distance) {
+ InitializeHoodPosition(constants::Values::kHoodRange().upper);
+
+ phased_loop_handle_ = event_loop_->AddPhasedLoop(
+ [this](int) {
+ // Skip this the first time.
+ if (!first_) {
+ Simulate();
+ }
+ first_ = false;
+ SendPositionMessage();
+ },
+ dt);
+ }
+
+ void InitializeHoodPosition(double start_pos) {
+ hood_plant_->mutable_X(0, 0) = start_pos;
+ hood_plant_->mutable_X(1, 0) = 0.0;
+
+ hood_encoder_.Initialize(
+ start_pos, kNoiseScalar, 0.0,
+ constants::GetValues()
+ .hood.zeroing_constants.measured_absolute_position);
+ }
+
+ // Sends a queue message with the position of the superstructure.
+ void SendPositionMessage() {
+ ::aos::Sender<Position>::Builder builder =
+ superstructure_position_sender_.MakeBuilder();
+
+ frc971::AbsolutePosition::Builder hood_builder =
+ builder.MakeBuilder<frc971::AbsolutePosition>();
+ flatbuffers::Offset<frc971::AbsolutePosition> hood_offset =
+ hood_encoder_.GetSensorValues(&hood_builder);
+
+ Position::Builder position_builder = builder.MakeBuilder<Position>();
+
+ position_builder.add_hood(hood_offset);
+
+ builder.Send(position_builder.Finish());
+ }
+
+ double hood_position() const { return hood_plant_->X(0, 0); }
+ double hood_velocity() const { return hood_plant_->X(1, 0); }
+
+ // Simulates the superstructure for a single timestep.
+ void Simulate() {
+ const double last_hood_velocity = hood_velocity();
+
+ EXPECT_TRUE(superstructure_output_fetcher_.Fetch());
+ EXPECT_TRUE(superstructure_status_fetcher_.Fetch());
+
+ const double voltage_check_hood =
+ (static_cast<AbsoluteEncoderSubsystem::State>(
+ superstructure_status_fetcher_->hood()->state()) ==
+ AbsoluteEncoderSubsystem::State::RUNNING)
+ ? constants::GetValues().hood.operating_voltage
+ : constants::GetValues().hood.zeroing_voltage;
+
+ EXPECT_NEAR(superstructure_output_fetcher_->hood_voltage(), 0.0,
+ voltage_check_hood);
+
+ ::Eigen::Matrix<double, 1, 1> hood_U;
+ hood_U << superstructure_output_fetcher_->hood_voltage() +
+ hood_plant_->voltage_offset();
+
+ hood_plant_->Update(hood_U);
+
+ const double position_hood = hood_plant_->Y(0, 0);
+
+ hood_encoder_.MoveTo(position_hood);
+
+ EXPECT_GE(position_hood, constants::Values::kHoodRange().lower_hard);
+ EXPECT_LE(position_hood, constants::Values::kHoodRange().upper_hard);
+
+ const double loop_time = ::aos::time::DurationInSeconds(dt_);
+
+ const double hood_acceleration =
+ (hood_velocity() - last_hood_velocity) / loop_time;
+
+ EXPECT_GE(peak_hood_acceleration_, hood_acceleration);
+ EXPECT_LE(-peak_hood_acceleration_, hood_acceleration);
+ EXPECT_GE(peak_hood_velocity_, hood_velocity());
+ EXPECT_LE(-peak_hood_velocity_, hood_velocity());
+ }
+
+ void set_peak_hood_acceleration(double value) {
+ peak_hood_acceleration_ = value;
+ }
+ void set_peak_hood_velocity(double value) { peak_hood_velocity_ = value; }
+
+ private:
+ ::aos::EventLoop *event_loop_;
+ const chrono::nanoseconds dt_;
+ ::aos::PhasedLoopHandler *phased_loop_handle_ = nullptr;
+
+ ::aos::Sender<Position> superstructure_position_sender_;
+ ::aos::Fetcher<Status> superstructure_status_fetcher_;
+ ::aos::Fetcher<Output> superstructure_output_fetcher_;
+
+ bool first_ = true;
+
+ ::std::unique_ptr<CappedTestPlant> hood_plant_;
+ PositionSensorSimulator hood_encoder_;
+
+ // The acceleration limits to check for while moving.
+ double peak_hood_acceleration_ = 1e10;
+
+ // The velocity limits to check for while moving.
+ double peak_hood_velocity_ = 1e10;
+};
+
+class SuperstructureTest : public ::aos::testing::ControlLoopTest {
+ protected:
+ SuperstructureTest()
+ : ::aos::testing::ControlLoopTest(
+ aos::configuration::ReadConfig("y2020/config.json"),
+ chrono::microseconds(5050)),
+ test_event_loop_(MakeEventLoop("test")),
+ superstructure_goal_fetcher_(
+ test_event_loop_->MakeFetcher<Goal>("/superstructure")),
+ superstructure_goal_sender_(
+ test_event_loop_->MakeSender<Goal>("/superstructure")),
+ superstructure_status_fetcher_(
+ test_event_loop_->MakeFetcher<Status>("/superstructure")),
+ superstructure_output_fetcher_(
+ test_event_loop_->MakeFetcher<Output>("/superstructure")),
+ superstructure_position_fetcher_(
+ test_event_loop_->MakeFetcher<Position>("/superstructure")),
+ superstructure_event_loop_(MakeEventLoop("superstructure")),
+ superstructure_(superstructure_event_loop_.get()),
+ superstructure_plant_event_loop_(MakeEventLoop("plant")),
+ superstructure_plant_(superstructure_plant_event_loop_.get(), dt()) {
+ set_team_id(::frc971::control_loops::testing::kTeamNumber);
+ }
+
+ void VerifyNearGoal() {
+ superstructure_goal_fetcher_.Fetch();
+ superstructure_status_fetcher_.Fetch();
+
+ EXPECT_NEAR(superstructure_goal_fetcher_->hood()->unsafe_goal(),
+ superstructure_status_fetcher_->hood()->position(), 0.001);
+ }
+
+ void CheckIfZeroed() {
+ superstructure_status_fetcher_.Fetch();
+ ASSERT_TRUE(superstructure_status_fetcher_.get()->zeroed());
+ }
+
+ void WaitUntilZeroed() {
+ int i = 0;
+ do {
+ i++;
+ RunFor(dt());
+ superstructure_status_fetcher_.Fetch();
+ // 2 Seconds
+ ASSERT_LE(i, 2.0 / ::aos::time::DurationInSeconds(dt()));
+
+ // Since there is a delay when sending running, make sure we have a status
+ // before checking it.
+ } while (superstructure_status_fetcher_.get() == nullptr ||
+ !superstructure_status_fetcher_.get()->zeroed());
+ }
+
+ ::std::unique_ptr<::aos::EventLoop> test_event_loop_;
+
+ ::aos::Fetcher<Goal> superstructure_goal_fetcher_;
+ ::aos::Sender<Goal> superstructure_goal_sender_;
+ ::aos::Fetcher<Status> superstructure_status_fetcher_;
+ ::aos::Fetcher<Output> superstructure_output_fetcher_;
+ ::aos::Fetcher<Position> superstructure_position_fetcher_;
+
+ // Create a control loop and simulation.
+ ::std::unique_ptr<::aos::EventLoop> superstructure_event_loop_;
+ Superstructure superstructure_;
+
+ ::std::unique_ptr<::aos::EventLoop> superstructure_plant_event_loop_;
+ SuperstructureSimulation superstructure_plant_;
+};
+
+// Tests that the superstructure does nothing when the goal is to remain still.
+TEST_F(SuperstructureTest, DoesNothing) {
+ SetEnabled(true);
+ superstructure_plant_.InitializeHoodPosition(0.77);
+
+ WaitUntilZeroed();
+
+ {
+ auto builder = superstructure_goal_sender_.MakeBuilder();
+
+ flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+ hood_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+ *builder.fbb(), 0.77);
+
+ Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+ goal_builder.add_hood(hood_offset);
+
+ ASSERT_TRUE(builder.Send(goal_builder.Finish()));
+ }
+ RunFor(chrono::seconds(10));
+ VerifyNearGoal();
+
+ EXPECT_TRUE(superstructure_output_fetcher_.Fetch());
+}
+
+// Tests that loops can reach a goal.
+TEST_F(SuperstructureTest, ReachesGoal) {
+ SetEnabled(true);
+ // Set a reasonable goal.
+
+ superstructure_plant_.InitializeHoodPosition(0.7);
+
+ WaitUntilZeroed();
+ {
+ auto builder = superstructure_goal_sender_.MakeBuilder();
+
+ flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+ hood_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+ *builder.fbb(), 0.2,
+ CreateProfileParameters(*builder.fbb(), 1.0, 0.2));
+
+ Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+ goal_builder.add_hood(hood_offset);
+
+ ASSERT_TRUE(builder.Send(goal_builder.Finish()));
+ }
+
+ // Give it a lot of time to get there.
+ RunFor(chrono::seconds(8));
+
+ VerifyNearGoal();
+}
+// Makes sure that the voltage on a motor is properly pulled back after
+// saturation such that we don't get weird or bad (e.g. oscillating) behaviour.
+//
+// We are going to disable collision detection to make this easier to implement.
+TEST_F(SuperstructureTest, SaturationTest) {
+ SetEnabled(true);
+ // Zero it before we move.
+ WaitUntilZeroed();
+ {
+ auto builder = superstructure_goal_sender_.MakeBuilder();
+
+ flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+ hood_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+ *builder.fbb(), constants::Values::kHoodRange().upper);
+
+ Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+ goal_builder.add_hood(hood_offset);
+
+ ASSERT_TRUE(builder.Send(goal_builder.Finish()));
+ }
+ RunFor(chrono::seconds(8));
+ VerifyNearGoal();
+
+ // Try a low acceleration move with a high max velocity and verify the
+ // acceleration is capped like expected.
+ {
+ auto builder = superstructure_goal_sender_.MakeBuilder();
+
+ flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
+ hood_offset = CreateStaticZeroingSingleDOFProfiledSubsystemGoal(
+ *builder.fbb(), constants::Values::kHoodRange().lower,
+ CreateProfileParameters(*builder.fbb(), 20.0, 0.1));
+
+ Goal::Builder goal_builder = builder.MakeBuilder<Goal>();
+
+ goal_builder.add_hood(hood_offset);
+
+ ASSERT_TRUE(builder.Send(goal_builder.Finish()));
+ }
+ superstructure_plant_.set_peak_hood_velocity(23.0);
+ superstructure_plant_.set_peak_hood_acceleration(0.2);
+
+ RunFor(chrono::seconds(8));
+ VerifyNearGoal();
+}
+
+// Tests that the loop zeroes when run for a while without a goal.
+TEST_F(SuperstructureTest, ZeroNoGoal) {
+ SetEnabled(true);
+ WaitUntilZeroed();
+ RunFor(chrono::seconds(2));
+ EXPECT_EQ(AbsoluteEncoderSubsystem::State::RUNNING,
+ superstructure_.hood().state());
+}
+
+// Tests that running disabled works
+TEST_F(SuperstructureTest, DisableTest) {
+ RunFor(chrono::seconds(2));
+ CheckIfZeroed();
+}
+
+} // namespace testing
+} // namespace superstructure
+} // namespace control_loops
+} // namespace y2020
diff --git a/yarn.lock b/yarn.lock
index 5fb2c43..2a9ba96 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -30,6 +30,11 @@
source-map-support "0.5.9"
tsutils "2.27.2"
+"@types/flatbuffers@latest":
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/@types/flatbuffers/-/flatbuffers-1.10.0.tgz#aa74e30ffdc86445f2f060e1808fc9d56b5603ba"
+ integrity sha512-7btbphLrKvo5yl/5CC2OCxUSMx1wV1wvGT1qDXkSt7yi00/YW7E8k6qzXqJHsp+WU0eoG7r6MTQQXI9lIvd0qA==
+
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"