tests: face system benchmark

Change-Id: Ia2d9125486c57f949e5c855ae6b8d95ffd5aa6e3
Refs: #3567
diff --git a/core/extended-error-message.hpp b/core/extended-error-message.hpp
index 10e62e2..f1443bd 100644
--- a/core/extended-error-message.hpp
+++ b/core/extended-error-message.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /**
- * Copyright (c) 2014-2015,  Regents of the University of California,
+ * Copyright (c) 2014-2016,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,6 +27,7 @@
 #define NFD_CORE_EXTENDED_ERROR_MESSAGE_HPP
 
 #include <boost/exception/get_error_info.hpp>
+#include <sstream>
 
 namespace nfd {
 
diff --git a/tests/other/face-benchmark.conf.sample b/tests/other/face-benchmark.conf.sample
new file mode 100644
index 0000000..3048341
--- /dev/null
+++ b/tests/other/face-benchmark.conf.sample
@@ -0,0 +1,2 @@
+udp4://192.0.2.2:6363 udp4://192.0.2.3:6363
+tcp4://192.0.2.4:6363 tcp4://192.0.2.5:6363
diff --git a/tests/other/face-benchmark.cpp b/tests/other/face-benchmark.cpp
new file mode 100644
index 0000000..afd4cf7
--- /dev/null
+++ b/tests/other/face-benchmark.cpp
@@ -0,0 +1,190 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "core/extended-error-message.hpp"
+#include "core/global-io.hpp"
+#include "face/face.hpp"
+#include "face/tcp-channel.hpp"
+#include "face/udp-channel.hpp"
+
+#include <fstream>
+#include <iostream>
+
+namespace nfd {
+namespace tests {
+
+class FaceBenchmark
+{
+public:
+  FaceBenchmark(const char* configFileName)
+    : m_terminationSignalSet{getGlobalIoService()}
+    , m_tcpChannel{tcp::Endpoint{boost::asio::ip::tcp::v4(), 6363}}
+    , m_udpChannel{udp::Endpoint{boost::asio::ip::udp::v4(), 6363}, time::minutes{10}}
+  {
+    m_terminationSignalSet.add(SIGINT);
+    m_terminationSignalSet.add(SIGTERM);
+    m_terminationSignalSet.async_wait(bind(&FaceBenchmark::terminate, _1, _2));
+
+    parseConfig(configFileName);
+
+    m_tcpChannel.listen(bind(&FaceBenchmark::onLeftFaceCreated, this, _1),
+                        bind(&FaceBenchmark::onFaceCreationFailed, _1));
+    std::clog << "Listening on " << m_tcpChannel.getUri() << std::endl;
+
+    m_udpChannel.listen(bind(&FaceBenchmark::onLeftFaceCreated, this, _1),
+                        bind(&FaceBenchmark::onFaceCreationFailed, _1));
+    std::clog << "Listening on " << m_udpChannel.getUri() << std::endl;
+  }
+
+private:
+  static void
+  terminate(const boost::system::error_code& error, int signalNo)
+  {
+    if (error)
+      return;
+    getGlobalIoService().stop();
+  }
+
+  void
+  parseConfig(const char* configFileName)
+  {
+    std::ifstream file{configFileName};
+    std::string uriStrL;
+    std::string uriStrR;
+
+    while (file >> uriStrL >> uriStrR) {
+      FaceUri uriL{uriStrL};
+      FaceUri uriR{uriStrR};
+
+      if (uriL.getScheme() != "tcp4" && uriL.getScheme() != "udp4") {
+        std::clog << "Unsupported protocol '" << uriL.getScheme() << "'" << std::endl;
+      }
+      else if (uriR.getScheme() != "tcp4" && uriR.getScheme() != "udp4") {
+        std::clog << "Unsupported protocol '" << uriR.getScheme() << "'" << std::endl;
+      }
+      else {
+        m_faceUris.push_back(std::make_pair(uriL, uriR));
+      }
+    }
+
+    if (m_faceUris.empty()) {
+      BOOST_THROW_EXCEPTION(std::runtime_error("No supported FaceUri pairs found in config file"));
+    }
+  }
+
+  void
+  onLeftFaceCreated(const shared_ptr<Face>& faceL)
+  {
+    std::clog << "Left face created: remote=" << faceL->getRemoteUri()
+              << " local=" << faceL->getLocalUri() << std::endl;
+
+    // find a matching right uri
+    FaceUri uriR;
+    for (const auto& pair : m_faceUris) {
+      if (pair.first.getHost() == faceL->getRemoteUri().getHost() &&
+          pair.first.getScheme() == faceL->getRemoteUri().getScheme()) {
+        uriR = pair.second;
+      }
+      else if (pair.second.getHost() == faceL->getRemoteUri().getHost() &&
+               pair.second.getScheme() == faceL->getRemoteUri().getScheme()) {
+        uriR = pair.first;
+      }
+    }
+
+    if (uriR == FaceUri()) {
+      std::clog << "No FaceUri matched, ignoring..." << std::endl;
+      faceL->close();
+      return;
+    }
+
+    // create the right face
+    auto addr = boost::asio::ip::address::from_string(uriR.getHost());
+    auto port = boost::lexical_cast<uint16_t>(uriR.getPort());
+    if (uriR.getScheme() == "tcp4") {
+      m_tcpChannel.connect(tcp::Endpoint(addr, port),
+                           bind(&FaceBenchmark::onRightFaceCreated, this, faceL, _1),
+                           bind(&FaceBenchmark::onFaceCreationFailed, _1));
+    }
+    else if (uriR.getScheme() == "udp4") {
+      m_udpChannel.connect(udp::Endpoint(addr, port),
+                           ndn::nfd::FACE_PERSISTENCY_PERSISTENT,
+                           bind(&FaceBenchmark::onRightFaceCreated, this, faceL, _1),
+                           bind(&FaceBenchmark::onFaceCreationFailed, _1));
+    }
+  }
+
+  void
+  onRightFaceCreated(const shared_ptr<Face>& faceL, const shared_ptr<Face>& faceR)
+  {
+    std::clog << "Right face created: remote=" << faceR->getRemoteUri()
+              << " local=" << faceR->getLocalUri() << std::endl;
+
+    tieFaces(faceR, faceL);
+    tieFaces(faceL, faceR);
+  }
+
+  static void
+  tieFaces(const shared_ptr<Face>& face1, const shared_ptr<Face>& face2)
+  {
+    face1->afterReceiveInterest.connect([face2] (const Interest& interest) { face2->sendInterest(interest); });
+    face1->afterReceiveData.connect([face2] (const Data& data) { face2->sendData(data); });
+    face1->afterReceiveNack.connect([face2] (const ndn::lp::Nack& nack) { face2->sendNack(nack); });
+  }
+
+  static void
+  onFaceCreationFailed(const std::string& reason)
+  {
+    BOOST_THROW_EXCEPTION(std::runtime_error("Failed to create face: " + reason));
+  }
+
+private:
+  boost::asio::signal_set m_terminationSignalSet;
+  TcpChannel m_tcpChannel;
+  UdpChannel m_udpChannel;
+  std::vector<std::pair<FaceUri, FaceUri>> m_faceUris;
+};
+
+} // namespace tests
+} // namespace nfd
+
+int
+main(int argc, char** argv)
+{
+  if (argc != 2) {
+    std::cerr << "Usage: " << argv[0] << " <config-file>" << std::endl;
+    return 2;
+  }
+
+  try {
+    nfd::tests::FaceBenchmark bench{argv[1]};
+    nfd::getGlobalIoService().run();
+  }
+  catch (const std::exception& e) {
+    std::cerr << "FATAL: " << nfd::getExtendedErrorMessage(e) << std::endl;
+    return 1;
+  }
+
+  return 0;
+}
diff --git a/tests/other/face-benchmark.md b/tests/other/face-benchmark.md
new file mode 100644
index 0000000..ae91432
--- /dev/null
+++ b/tests/other/face-benchmark.md
@@ -0,0 +1,23 @@
+# Face Benchmark
+
+**face-benchmark** is a program to test the performance of the face system. It runs
+on a node between n consumer nodes and n producer nodes. It creates a left face and
+a right face for each pair of consumer node and producer node respectively. The left
+and right faces in each pair are tied to each other at the Interest/Data/Nack layer,
+which means that anything received on the left face is sent to the corresponding
+right face, and anything received on the right face is sent to the corresponding
+left face. The program passively waits for an incoming connection from one side which
+triggers an outgoing connection to the other side. The FacePersistency is set to
+"on-demand" for all incoming faces, and "persistent" for all outgoing faces.
+
+The FaceUris for each face pair can be configured via a configuration file. Each
+line of the configuration file consists of a left FaceUri and a right FaceUri
+separated by a space. FaceUri schemes "tcp4" and "udp4" are supported. The left face
+and right face are allowed to have different FaceUri schemes. All FaceUris MUST be
+in canonical form.
+
+Usage example:
+
+1. Configure FaceUris in `face-benchmark.conf`
+2. On the router node, run `./face-benchmark face-benchmark.conf`
+3. Run NFD on the consumer/producer node pairs
diff --git a/tests/other/wscript b/tests/other/wscript
index 95fd9c5..926ba9d 100644
--- a/tests/other/wscript
+++ b/tests/other/wscript
@@ -1,7 +1,7 @@
 # -*- Mode: python; py-indent-offset: 4; indent-tabs-mode: nil; coding: utf-8; -*-
 
 """
-Copyright (c) 2014-2015,  Regents of the University of California,
+Copyright (c) 2014-2016,  Regents of the University of California,
                           Arizona Board of Regents,
                           Colorado State University,
                           University Pierre & Marie Curie, Sorbonne University,
@@ -27,27 +27,32 @@
 top = '../..'
 
 def build(bld):
-   testPrograms = {
-      "cs-benchmark": "CS Benchmark",
-      "pit-fib-benchmark": "PIT & FIB Benchmark",
-   }
-   for module, name in testPrograms.items():
-       # main()
-       bld(target='unit-tests-%s-main' % module,
-           name='unit-tests-%s-main' % module,
-           features='cxx',
-           use='BOOST',
-           source='../main.cpp',
-           defines=['BOOST_TEST_MODULE=%s' % name]
-         )
+    for module, name in {"cs-benchmark": "CS Benchmark",
+                         "pit-fib-benchmark": "PIT & FIB Benchmark"}.items():
+        # main
+        bld(target='unit-tests-%s-main' % module,
+            name='unit-tests-%s-main' % module,
+            features='cxx',
+            source='../main.cpp',
+            use='BOOST',
+            defines=['BOOST_TEST_MODULE=%s' % name]
+            )
 
-       # unit-tests-%module
-       bld.program(
-           target='../../%s' % module,
-           features='cxx cxxprogram',
-           source=bld.path.ant_glob(['%s*.cpp' % module]),
-           use='daemon-objects unit-tests-base unit-tests-%s-main' % module,
-           includes='.',
-           install_path=None,
-           defines='UNIT_TEST_CONFIG_PATH=\"%s/tmp-files/\"' % bld.bldnode,
-         )
+        # module
+        bld.program(target='../../%s' % module,
+                    features='cxx cxxprogram',
+                    source=bld.path.ant_glob(['%s*.cpp' % module]),
+                    use='daemon-objects unit-tests-base unit-tests-%s-main' % module,
+                    includes='.',
+                    install_path=None,
+                    defines=['UNIT_TEST_CONFIG_PATH=\"%s/tmp-files/\"' % bld.bldnode]
+                    )
+
+    # face-benchmark does not rely on Boost.Test
+    bld.program(target='../../face-benchmark',
+                features='cxx cxxprogram',
+                source=bld.path.ant_glob(['face-benchmark*.cpp']),
+                use='daemon-objects',
+                includes='.',
+                install_path=None
+                )