diff --git a/cmake/PugsDoc.cmake b/cmake/PugsDoc.cmake
index b067a5cc828ec0c245e8d51c0bda46a96dcce702..2be770b3a3163c6ea490413b0f73a1f4f828c82f 100644
--- a/cmake/PugsDoc.cmake
+++ b/cmake/PugsDoc.cmake
@@ -97,7 +97,19 @@ if (EMACS AND GNUPLOT_FOUND AND GMSH)
       COMMENT "Building user documentation in doc/userdoc.pdf"
       VERBATIM)
 
-    add_custom_target(userdoc-pdf DEPENDS pugsdoc-dir "${PUGS_BINARY_DIR}/doc/userdoc.pdf" )
+    configure_file("${PUGS_SOURCE_DIR}/doc/build-userdoc-pdf.sh.in"
+      "${PUGS_BINARY_DIR}/doc/build-userdoc-pdf.sh"
+      FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
+      @ONLY)
+
+    set_source_files_properties(
+      ${PUGS_BINARY_DIR}/build-pdf.sh2
+      PROPERTIES
+      GENERATED TRUE
+      HEADER_FILE_ONLY TRUE
+    )
+
+    add_custom_target(userdoc-pdf DEPENDS pugsdoc-dir "${PUGS_BINARY_DIR}/doc/userdoc.pdf" "${PUGS_BINARY_DIR}/doc/build-userdoc-pdf.sh")
 
     add_dependencies(userdoc userdoc-pdf)
 
diff --git a/doc/build-userdoc-pdf.sh.in b/doc/build-userdoc-pdf.sh.in
new file mode 100644
index 0000000000000000000000000000000000000000..9445e7e75cbb62486a4aa3647fa41a171adb7087
--- /dev/null
+++ b/doc/build-userdoc-pdf.sh.in
@@ -0,0 +1,5 @@
+#! /usr/bin/env bash
+
+@PDFLATEX_COMPILER@ -shell-escape -interaction nonstopmode userdoc
+@PDFLATEX_COMPILER@ -shell-escape -interaction nonstopmode userdoc
+@PDFLATEX_COMPILER@ -shell-escape -interaction nonstopmode userdoc
diff --git a/doc/lisp/build-doc-config.el b/doc/lisp/build-doc-config.el
index 1da3a5c9fecf3540dfd701380a340722379642cf..6f06400a6dc1176370550342ea7bfb04ac976c11 100644
--- a/doc/lisp/build-doc-config.el
+++ b/doc/lisp/build-doc-config.el
@@ -64,9 +64,7 @@
 (setq org-latex-listings 'minted
       org-latex-packages-alist '(("" "minted"))
       org-latex-pdf-process
-      '("cd ${PUGS_BINARY_DIR}/doc; pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
-        "cd ${PUGS_BINARY_DIR}/doc; pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
-        "cd ${PUGS_BINARY_DIR}/doc; pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"))
+      '("cd ${PUGS_BINARY_DIR}/doc; ./build-userdoc-pdf.sh"))
 
 (setq python-indent-guess-indent-offset-verbose nil)
 
diff --git a/doc/lisp/share/ob-pugs-error.el b/doc/lisp/share/ob-pugs-error.el
index 1e4680cbcc71729be805622081b37dfad6176d9c..e2228f9f4cc3ca0a0fcb65d32d8c2f8780419229 100644
--- a/doc/lisp/share/ob-pugs-error.el
+++ b/doc/lisp/share/ob-pugs-error.el
@@ -131,7 +131,7 @@
     (with-temp-file in-file
       (insert body))
     (org-babel-eval
-		(format "${PUGS} --no-preamble --no-color %s 2>&1 | sed 's@/.*\.pgs:@test.pgs:@'"
+		(format "${PUGS} --no-exec-stat --no-preamble --no-color --threads=1 %s 2>&1 | sed 's@/.*\.pgs:@test.pgs:@'"
 			(org-babel-process-file-name in-file))
 		"")))
 
diff --git a/doc/lisp/share/ob-pugs.el b/doc/lisp/share/ob-pugs.el
index 6c3d72956d519bea339b63cb883c57f9bf1cd000..fb346b80b68f95a10b214192a99c8a1e1d8f4a48 100644
--- a/doc/lisp/share/ob-pugs.el
+++ b/doc/lisp/share/ob-pugs.el
@@ -130,7 +130,7 @@
     (with-temp-file in-file
       (insert body))
     (org-babel-eval
-		(format "${PUGS} --no-preamble --no-color %s"
+		(format "${PUGS} --no-exec-stat --no-preamble --no-color --threads=1 %s"
 			(org-babel-process-file-name in-file))
 		"")))
 
diff --git a/doc/userdoc.org b/doc/userdoc.org
index 490db865223c800e05ee0ee31b52bd86486f8872..b80850ee590d3416622539b6aa4e9b3b9665d092 100644
--- a/doc/userdoc.org
+++ b/doc/userdoc.org
@@ -391,8 +391,8 @@ answer a specific need. It must not be done /because it is possible to
 do it/!
 
 #+begin_verse
-When designing a language, the difficulty is not to offer new functionalities,\\
-it is generally to decide not to offer them.\\
+When designing a language, the difficulty is not to offer new functionalities,
+it is generally to decide not to offer them.
 --- Bjarne Stroustrup, C++ conference 2021.
 #+end_verse
 
@@ -1390,7 +1390,7 @@ they follow a few rules.
   When comparing a boolean value (type ~B~) with another scalar value
   type (~N~, ~Z~ or ~R~), the value ~true~ is interpreted as $1$ and the value
   ~false~ as $0$.
-\\
+  \\
   For vector and matrix basic types, the only allowed operators are ~==~
   and ~!=~.
   #+begin_src latex :results drawer :exports results
@@ -1406,7 +1406,7 @@ they follow a few rules.
       \right.
     \end{equation*}
   #+end_src
-\\
+
   This is also the case for ~string~ values: only allowed operators are
   ~==~ and ~!=~.
   #+begin_src latex :results drawer :exports results
@@ -3136,7 +3136,6 @@ available in parallel
 
 ***** Item types
 
-\\
 The following functions are used to designate a specific ~item_type~
 - ~cell: void -> item_type~
 - ~face: void -> item_type~
diff --git a/src/main.cpp b/src/main.cpp
index 0cae9351ff97396484cf28f52c242692ce9299f5..9fc0b67439e128e958552a7b8c72cc354fc2076c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,12 +5,14 @@
 #include <mesh/DualMeshManager.hpp>
 #include <mesh/MeshDataManager.hpp>
 #include <mesh/SynchronizerManager.hpp>
+#include <utils/ExecutionStatManager.hpp>
 #include <utils/PugsUtils.hpp>
 #include <utils/RandomEngine.hpp>
 
 int
 main(int argc, char* argv[])
 {
+  ExecutionStatManager::create();
   ParallelChecker::create();
 
   std::string filename = initialize(argc, argv);
@@ -23,17 +25,19 @@ main(int argc, char* argv[])
   DualMeshManager::create();
 
   parser(filename);
+  ExecutionStatManager::printInfo();
 
   DualMeshManager::destroy();
   DualConnectivityManager::destroy();
   MeshDataManager::destroy();
-  RandomEngine::destroy();
   QuadratureManager::destroy();
+  RandomEngine::destroy();
   SynchronizerManager::destroy();
 
   finalize();
 
   ParallelChecker::destroy();
+  ExecutionStatManager::destroy();
 
   return 0;
 }
diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt
index ca07c7dc6882942600e2c5e9b216ca1998fbbd71..82b31ae419fcb2f16b314aef5d4d55b4bb83e2fb 100644
--- a/src/utils/CMakeLists.txt
+++ b/src/utils/CMakeLists.txt
@@ -8,6 +8,7 @@ add_library(
   ConsoleManager.cpp
   Demangle.cpp
   Exceptions.cpp
+  ExecutionStatManager.cpp
   FPEManager.cpp
   Messenger.cpp
   Partitioner.cpp
diff --git a/src/utils/ExecutionStatManager.cpp b/src/utils/ExecutionStatManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8fbd32e2c3a200808cd91d26fd845df7a1eb256a
--- /dev/null
+++ b/src/utils/ExecutionStatManager.cpp
@@ -0,0 +1,150 @@
+#include <utils/ExecutionStatManager.hpp>
+
+#include <utils/Exceptions.hpp>
+#include <utils/Messenger.hpp>
+
+#include <cmath>
+#include <iomanip>
+#include <rang.hpp>
+#include <sys/resource.h>
+
+ExecutionStatManager* ExecutionStatManager::m_instance = nullptr;
+
+void
+ExecutionStatManager::_printMaxResidentMemory() const
+{
+  class Memory
+  {
+   private:
+    double m_value;
+
+   public:
+    PUGS_INLINE const double&
+    value() const
+    {
+      return m_value;
+    }
+
+    std::string
+    prettyPrint() const
+    {
+      const std::vector<std::string> units = {"B", "KB", "MB", "GB", "TB", "PB", "EB"};
+
+      double local_memory = m_value;
+      size_t i_unit       = 0;
+      while ((local_memory >= 1024) and (i_unit < units.size())) {
+        ++i_unit;
+        local_memory /= 1024;
+      }
+      std::ostringstream os;
+      os << local_memory << units[i_unit];
+      return os.str();
+    }
+
+    Memory()
+    {
+      rusage u;
+      getrusage(RUSAGE_SELF, &u);
+      m_value = u.ru_maxrss * 1024;
+    }
+
+    Memory(double value) : m_value{value} {}
+  };
+
+  Memory memory;
+  std::cout << "Memory: " << rang::style::bold << Memory{parallel::allReduceSum(memory.value())}.prettyPrint()
+            << rang::style::reset;
+  if (parallel::size() > 1) {
+    std::cout << " (over " << parallel::size() << " processes)";
+    std::cout << " Avg: " << rang::style::bold
+              << Memory{parallel::allReduceSum(memory.value()) / parallel::size()}.prettyPrint() << rang::style::reset;
+    std::cout << " Min: " << rang::style::bold << Memory{parallel::allReduceMin(memory.value())}.prettyPrint()
+              << rang::style::reset;
+    std::cout << " Max: " << rang::style::bold << Memory{parallel::allReduceMax(memory.value())}.prettyPrint()
+              << rang::style::reset;
+  }
+  std::cout << '\n';
+}
+
+void
+ExecutionStatManager::_printElapseTime() const
+{
+  std::cout << "Execution: " << rang::style::bold << m_instance->m_elapse_time.seconds() << 's' << rang::style::reset
+            << '\n';
+}
+
+void
+ExecutionStatManager::_printTotalCPUTime() const
+{
+  rusage u;
+  getrusage(RUSAGE_SELF, &u);
+
+  const double total_cpu_time =
+    u.ru_utime.tv_sec + u.ru_stime.tv_sec + (u.ru_utime.tv_usec + u.ru_stime.tv_usec) * 1E-6;
+
+  std::cout << "Total CPU: " << rang::style::bold << parallel::allReduceSum(total_cpu_time) << 's'
+            << rang::style::reset;
+  std::cout << " (" << parallel::allReduceSum(Kokkos::DefaultHostExecutionSpace::concurrency()) << " threads over "
+            << parallel::size() << " processes)";
+  if (total_cpu_time > 60) {
+    size_t seconds    = std::floor(total_cpu_time);
+    const size_t days = seconds / (24 * 3600);
+    seconds -= days * (24 * 3600);
+    const size_t hours = seconds / 3600;
+    seconds -= hours * 3600;
+    const size_t minutes = seconds / 60;
+    seconds -= minutes * 60;
+    std::cout << " " << rang::style::bold;
+    bool print = false;
+    if (days > 0) {
+      print = true;
+      std::cout << days << "d" << ' ';
+    }
+    if (print or (hours > 0)) {
+      print = true;
+      std::cout << std::setw(2) << std::setfill('0') << hours << "h";
+    }
+    if (print or (minutes > 0)) {
+      print = true;
+      std::cout << std::setw(2) << std::setfill('0') << minutes << "mn";
+    }
+    if (print) {
+      std::cout << rang::style::bold << std::setw(2) << std::setfill('0') << seconds << "s";
+    }
+    std::cout << rang::style::reset;
+  }
+  std::cout << '\n';
+}
+
+void
+ExecutionStatManager::printInfo()
+{
+  if (ExecutionStatManager::getInstance().doPrint()) {
+    std::cout << "----------------- " << rang::fg::green << "pugs exec stats" << rang::fg::reset
+              << " ---------------------\n";
+
+    ExecutionStatManager::getInstance()._printElapseTime();
+    ExecutionStatManager::getInstance()._printTotalCPUTime();
+    ExecutionStatManager::getInstance()._printMaxResidentMemory();
+  }
+}
+
+void
+ExecutionStatManager::create()
+{
+  if (ExecutionStatManager::m_instance == nullptr) {
+    ExecutionStatManager::m_instance = new ExecutionStatManager;
+  } else {
+    throw UnexpectedError("ExecutionStatManager already created");
+  }
+}
+
+void
+ExecutionStatManager::destroy()
+{
+  // One allows multiple destruction to handle unexpected code exit
+  if (ExecutionStatManager::m_instance != nullptr) {
+    delete ExecutionStatManager::m_instance;
+    ExecutionStatManager::m_instance = nullptr;
+  }
+}
diff --git a/src/utils/ExecutionStatManager.hpp b/src/utils/ExecutionStatManager.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..476b31a84f80234a6e8d509ddac88daf9d3b38f8
--- /dev/null
+++ b/src/utils/ExecutionStatManager.hpp
@@ -0,0 +1,52 @@
+#ifndef EXECUTION_STAT_MANAGER_HPP
+#define EXECUTION_STAT_MANAGER_HPP
+
+#include <utils/PugsAssert.hpp>
+#include <utils/Timer.hpp>
+
+class ExecutionStatManager
+{
+ private:
+  static ExecutionStatManager* m_instance;
+
+  Timer m_elapse_time;
+  bool m_do_print = true;
+
+  void _printMaxResidentMemory() const;
+  void _printElapseTime() const;
+  void _printTotalCPUTime() const;
+
+  explicit ExecutionStatManager()                   = default;
+  ExecutionStatManager(ExecutionStatManager&&)      = delete;
+  ExecutionStatManager(const ExecutionStatManager&) = delete;
+  ~ExecutionStatManager()                           = default;
+
+ public:
+  PUGS_INLINE
+  bool
+  doPrint() const
+  {
+    return m_do_print;
+  }
+
+  PUGS_INLINE
+  void
+  setPrint(bool do_print)
+  {
+    m_do_print = do_print;
+  }
+
+  PUGS_INLINE
+  static ExecutionStatManager&
+  getInstance()
+  {
+    Assert(m_instance != nullptr);   // LCOV_EXCL_LINE
+    return *m_instance;
+  }
+
+  static void printInfo();
+  static void create();
+  static void destroy();
+};
+
+#endif   // EXECUTION_STAT_MANAGER_HPP
diff --git a/src/utils/PugsUtils.cpp b/src/utils/PugsUtils.cpp
index e4e905765f964b0760a10fa3349315e7ce15459d..2919ff18866dce74847bf672b61f0200f6a77dc6 100644
--- a/src/utils/PugsUtils.cpp
+++ b/src/utils/PugsUtils.cpp
@@ -5,6 +5,7 @@
 #include <utils/BuildInfo.hpp>
 #include <utils/CommunicatorManager.hpp>
 #include <utils/ConsoleManager.hpp>
+#include <utils/ExecutionStatManager.hpp>
 #include <utils/FPEManager.hpp>
 #include <utils/Messenger.hpp>
 #include <utils/PETScWrapper.hpp>
@@ -115,8 +116,12 @@ initialize(int& argc, char* argv[])
     bool show_preamble = true;
     app.add_flag("--preamble,!--no-preamble", show_preamble, "Show execution info preamble [default: true]");
 
-    bool show_backtrace = false;
-    app.add_flag("-b,--backtrace,!--no-backtrace", show_backtrace, "Show backtrace on failure [default: false]");
+    bool print_exec_stat = true;
+    app.add_flag("--exec-stat,!--no-exec-stat", print_exec_stat,
+                 "Display memory and CPU usage after execution [default: true]");
+
+    bool show_backtrace = true;
+    app.add_flag("-b,--backtrace,!--no-backtrace", show_backtrace, "Show backtrace on failure [default: true]");
 
     app.add_flag("--signal,!--no-signal", enable_signals, "Catches signals [default: true]");
 
@@ -157,6 +162,7 @@ initialize(int& argc, char* argv[])
       CommunicatorManager::setSplitColor(mpi_split_color);
     }
 
+    ExecutionStatManager::getInstance().setPrint(print_exec_stat);
     BacktraceManager::setShow(show_backtrace);
     ConsoleManager::setShowPreamble(show_preamble);
     ConsoleManager::init(enable_color);