diff --git a/CMakeLists.txt b/CMakeLists.txt index da8d5d76..3db0d7b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,6 +214,7 @@ set(XEUS_CPP_SRC src/xparser.cpp src/xutils.cpp src/xmagics/os.cpp + src/xmagics/execution.cpp ) if(NOT EMSCRIPTEN) diff --git a/src/xinterpreter.cpp b/src/xinterpreter.cpp index 85aa07b1..6cc2dab0 100644 --- a/src/xinterpreter.cpp +++ b/src/xinterpreter.cpp @@ -17,6 +17,7 @@ #include "xinput.hpp" #include "xinspect.hpp" +#include "xmagics/execution.hpp" #include "xmagics/os.hpp" #ifndef EMSCRIPTEN #include "xmagics/xassist.hpp" @@ -26,47 +27,66 @@ using Args = std::vector; -void* createInterpreter(const Args &ExtraArgs = {}) { - Args ClangArgs = {/*"-xc++"*/"-v"}; // ? {"-Xclang", "-emit-llvm-only", "-Xclang", "-diagnostic-log-file", "-Xclang", "-", "-xc++"}; +void* createInterpreter(const Args& ExtraArgs = {}) +{ + Args ClangArgs = {/*"-xc++"*/ "-v"}; // ? {"-Xclang", "-emit-llvm-only", "-Xclang", + // "-diagnostic-log-file", "-Xclang", "-", "-xc++"}; #ifdef EMSCRIPTEN - ClangArgs.push_back("-std=c++20"); + ClangArgs.push_back("-std=c++20"); #else - if (std::find_if(ExtraArgs.begin(), ExtraArgs.end(), [](const std::string& s) { - return s == "-resource-dir";}) == ExtraArgs.end()) { - std::string resource_dir = Cpp::DetectResourceDir(); - if (resource_dir.empty()) - std::cerr << "Failed to detect the resource-dir\n"; - ClangArgs.push_back("-resource-dir"); - ClangArgs.push_back(resource_dir.c_str()); - } - std::vector CxxSystemIncludes; - Cpp::DetectSystemCompilerIncludePaths(CxxSystemIncludes); - for (const std::string& CxxInclude : CxxSystemIncludes) { - ClangArgs.push_back("-isystem"); - ClangArgs.push_back(CxxInclude.c_str()); - } + if (std::find_if( + ExtraArgs.begin(), + ExtraArgs.end(), + [](const std::string& s) + { + return s == "-resource-dir"; + } + ) + == ExtraArgs.end()) + { + std::string resource_dir = Cpp::DetectResourceDir(); + if (resource_dir.empty()) + { + std::cerr << "Failed to detect the resource-dir\n"; + } + ClangArgs.push_back("-resource-dir"); + ClangArgs.push_back(resource_dir.c_str()); + } + std::vector CxxSystemIncludes; + Cpp::DetectSystemCompilerIncludePaths(CxxSystemIncludes); + for (const std::string& CxxInclude : CxxSystemIncludes) + { + ClangArgs.push_back("-isystem"); + ClangArgs.push_back(CxxInclude.c_str()); + } #endif - ClangArgs.insert(ClangArgs.end(), ExtraArgs.begin(), ExtraArgs.end()); - // FIXME: We should process the kernel input options and conditionally pass - // the gpu args here. - return Cpp::CreateInterpreter(ClangArgs/*, {"-cuda"}*/); + ClangArgs.insert(ClangArgs.end(), ExtraArgs.begin(), ExtraArgs.end()); + // FIXME: We should process the kernel input options and conditionally pass + // the gpu args here. + return Cpp::CreateInterpreter(ClangArgs /*, {"-cuda"}*/); } using namespace std::placeholders; namespace xcpp { - struct StreamRedirectRAII { - std::string &err; - StreamRedirectRAII(std::string &e) : err(e) { - Cpp::BeginStdStreamCapture(Cpp::kStdErr); - Cpp::BeginStdStreamCapture(Cpp::kStdOut); - } - ~StreamRedirectRAII() { - std::string out = Cpp::EndStdStreamCapture(); - err = Cpp::EndStdStreamCapture(); - std::cout << out; - } + struct StreamRedirectRAII + { + std::string& err; + + StreamRedirectRAII(std::string& e) + : err(e) + { + Cpp::BeginStdStreamCapture(Cpp::kStdErr); + Cpp::BeginStdStreamCapture(Cpp::kStdOut); + } + + ~StreamRedirectRAII() + { + std::string out = Cpp::EndStdStreamCapture(); + err = Cpp::EndStdStreamCapture(); + std::cout << out; + } }; void interpreter::configure_impl() @@ -101,14 +121,14 @@ __get_cxx_version () return std::to_string(cxx_version); } - interpreter::interpreter(int argc, const char* const* argv) : - xmagics() + interpreter::interpreter(int argc, const char* const* argv) + : xmagics() , p_cout_strbuf(nullptr) , p_cerr_strbuf(nullptr) , m_cout_buffer(std::bind(&interpreter::publish_stdout, this, _1)) , m_cerr_buffer(std::bind(&interpreter::publish_stderr, this, _1)) { - //NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) + // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) createInterpreter(Args(argv ? argv + 1 : argv, argv + argc)); m_version = get_stdopt(); redirect_output(); @@ -211,10 +231,11 @@ __get_cxx_version () // // JupyterLab displays the "{ename}: {evalue}" if the traceback is // empty. - if (evalue.size() < 4) { + if (evalue.size() < 4) + { ename = " "; } - std::vector traceback({ename + evalue}); + std::vector traceback({ename + evalue}); if (!config.silent) { publish_execution_error(ename, evalue, traceback); @@ -257,7 +278,8 @@ __get_cxx_version () Cpp::CodeComplete(results, code.c_str(), 1, _cursor_pos + 1); - return xeus::create_complete_reply(results /*matches*/, + return xeus::create_complete_reply( + results /*matches*/, cursor_pos - to_complete.length() /*cursor_start*/, cursor_pos /*cursor_end*/ ); @@ -278,13 +300,17 @@ __get_cxx_version () nl::json interpreter::is_complete_request_impl(const std::string& code) { - if (!code.empty() && code[code.size() - 1] == '\\') { + if (!code.empty() && code[code.size() - 1] == '\\') + { auto found = code.rfind('\n'); if (found == std::string::npos) + { found = -1; + } auto found1 = found++; - while (isspace(code[++found1])) ; - return xeus::create_is_complete_reply("incomplete", code.substr(found, found1-found)); + while (isspace(code[++found1])) + ; + return xeus::create_is_complete_reply("incomplete", code.substr(found, found1 - found)); } return xeus::create_is_complete_reply("complete"); @@ -358,11 +384,11 @@ __get_cxx_version () void interpreter::init_preamble() { - //NOLINTBEGIN(cppcoreguidelines-owning-memory) + // NOLINTBEGIN(cppcoreguidelines-owning-memory) preamble_manager.register_preamble("introspection", std::make_unique()); preamble_manager.register_preamble("magics", std::make_unique()); preamble_manager.register_preamble("shell", std::make_unique()); - //NOLINTEND(cppcoreguidelines-owning-memory) + // NOLINTEND(cppcoreguidelines-owning-memory) } void interpreter::init_magic() @@ -373,6 +399,7 @@ __get_cxx_version () // timeit(&m_interpreter)); // preamble_manager["magics"].get_cast().register_magic("python", pythonexec()); preamble_manager["magics"].get_cast().register_magic("file", writefile()); + preamble_manager["magics"].get_cast().register_magic("timeit", timeit()); #ifndef EMSCRIPTEN preamble_manager["magics"].get_cast().register_magic("xassist", xassist()); #endif diff --git a/src/xmagics/execution.cpp b/src/xmagics/execution.cpp new file mode 100644 index 00000000..c586399a --- /dev/null +++ b/src/xmagics/execution.cpp @@ -0,0 +1,262 @@ +/************************************************************************************ + * Copyright (c) 2023, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ + +#include "execution.hpp" +#include "xeus-cpp/xinterpreter.hpp" + +#include "../xparser.hpp" +#include "clang/Interpreter/CppInterOp.h" + +namespace xcpp +{ + struct StreamRedirectRAII + { + std::string& err; + + StreamRedirectRAII(std::string& e) + : err(e) + { + Cpp::BeginStdStreamCapture(Cpp::kStdErr); + Cpp::BeginStdStreamCapture(Cpp::kStdOut); + } + + ~StreamRedirectRAII() + { + std::string out = Cpp::EndStdStreamCapture(); + err = Cpp::EndStdStreamCapture(); + std::cout << out; + } + }; + + int timeit::exec_counter = 0; + + void timeit::operator()(const std::string& line) + { + std::string cline = line; + std::string ccell = ""; + execute(cline, ccell); + } + + void timeit::operator()(const std::string& line, const std::string& cell) + { + std::string cline = line; + std::string ccell = cell; + execute(cline, ccell); + } + + void timeit::get_options(argparser& argpars) + { + argpars.add_description("Time execution of a C++ statement or expression"); + argpars.add_argument("-n", "--number") + .help("execute the given statement n times in a loop. If this value is not given, a fitting value is chosen" + ) + .default_value(0) + .scan<'i', int>(); + argpars.add_argument("-r", "--repeat") + .help("repeat the loop iteration r times and take the best result") + .default_value(7) + .scan<'i', int>(); + argpars.add_argument("-p", "--precision") + .help("use a precision of p digits to display the timing result") + .default_value(3) + .scan<'i', int>(); + argpars.add_argument("expression").help("expression to be evaluated").remaining(); + argpars.add_argument("-h", "--help") + .action( + [&](const std::string& /*unused*/) + { + std::cout << argpars.help().str(); + } + ) + .default_value(false) + .help("shows help message") + .implicit_value(true) + .nargs(0); + } + + std::string timeit::inner(std::size_t number, const std::string& code, int exec_counter) const + { + static std::size_t counter = 0; // Ensure unique lambda names + std::string unique_id = std::to_string(counter++); + std::string timeit_code = ""; + timeit_code += "auto user_code_" + unique_id + " = []() {\n"; + timeit_code += " " + code + "\n"; + timeit_code += "};\n"; + timeit_code += "get_elapsed_time_" + std::to_string(exec_counter) + "(" + std::to_string(number) + + ", user_code_" + unique_id + ")\n"; + return timeit_code; + } + + std::string timeit::_format_time(double timespan, std::size_t precision) const + { + std::vector units{"s", "ms", "us", "ns"}; + std::vector scaling{1, 1e3, 1e6, 1e9}; + std::ostringstream output; + int order; + + if (timespan > 0.0) + { + order = std::min(-static_cast(std::floor(std::floor(std::log10(timespan)) / 3)), 3); + } + else + { + order = 3; + } + output.precision(precision); + output << timespan * scaling[order] << " " << units[order]; + return output.str(); + } + + void timeit::execute(std::string& line, std::string& cell) + { + exec_counter++; + argparser argpars("timeit", XEUS_CPP_VERSION, argparse::default_arguments::none); + get_options(argpars); + argpars.parse(line); + + int number = argpars.get("-n"); + int repeat = argpars.get("-r"); + int precision = argpars.get("-p"); + + std::string code; + try + { + const auto& v = argpars.get>("expression"); + for (const auto& s : v) + { + code += " " + s; + } + } + catch (std::logic_error& e) + { + if (trim(cell).empty() && (argpars["-h"] == false)) + { + std::cerr << "No expression given to evaluate" << std::endl; + } + } + + code += cell; + if (trim(code).empty()) + { + return; + } + + auto errorlevel = 0; + std::string ename; + std::string evalue; + std::string output; + std::string err; + bool hadError = false; + + bool compilation_result = true; + compilation_result = Cpp::Process("#include \n"); + // Define the reusable timing function once + std::string timing_function = R"( + double get_elapsed_time_)" + + std::to_string(exec_counter) + + R"( (std::size_t num_iterations, void (*func)()) { + auto _t2 = std::chrono::high_resolution_clock::now(); + for (std::size_t _i = 0; _i < num_iterations; ++_i) { + func(); + } + auto _t3 = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(_t3 - _t2).count(); + return duration < 0 ? 0.0 : static_cast(duration); + } + )"; + + compilation_result = Cpp::Process(timing_function.c_str()); + + try + { + StreamRedirectRAII R(err); + std::ostringstream buffer_out, buffer_err; + std::streambuf* old_cout = std::cout.rdbuf(buffer_out.rdbuf()); + std::streambuf* old_cerr = std::cerr.rdbuf(buffer_err.rdbuf()); + compilation_result = Cpp::Declare(code.c_str()); + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + } + catch (std::exception& e) + { + errorlevel = 1; + ename = "Standard Exception: "; + evalue = e.what(); + return; + } + catch (...) + { + errorlevel = 1; + ename = "Error: "; + return; + } + + if (compilation_result) + { + errorlevel = 1; + ename = "Error: "; + evalue = "Compilation error! " + err; + std::cerr << err; + return; + } + + + if (number == 0) + { + for (std::size_t n = 0; n < 10; ++n) + { + number = std::pow(10, n); + std::string timeit_code = inner(number, code, exec_counter); + std::ostringstream buffer_out, buffer_err; + std::streambuf* old_cout = std::cout.rdbuf(buffer_out.rdbuf()); + std::streambuf* old_cerr = std::cerr.rdbuf(buffer_err.rdbuf()); + StreamRedirectRAII R(err); + auto res_ptr = Cpp::Evaluate(timeit_code.c_str(), &hadError); + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + output = std::to_string(res_ptr); + err += buffer_err.str(); + double elapsed_time = std::stod(output) * 1e-6; + if (elapsed_time >= 0.2) + { + break; + } + } + } + + std::vector all_runs; + double mean = 0; + double stdev = 0; + for (std::size_t r = 0; r < static_cast(repeat); ++r) + { + std::string timeit_code = inner(number, code, exec_counter); + std::ostringstream buffer_out, buffer_err; + std::streambuf* old_cout = std::cout.rdbuf(buffer_out.rdbuf()); + std::streambuf* old_cerr = std::cerr.rdbuf(buffer_err.rdbuf()); + StreamRedirectRAII R(err); + auto res_ptr = Cpp::Evaluate(timeit_code.c_str(), &hadError); + std::cout.rdbuf(old_cout); + std::cerr.rdbuf(old_cerr); + output = std::to_string(res_ptr); + err += buffer_err.str(); + double elapsed_time = std::stod(output) * 1e-6; + all_runs.push_back(elapsed_time / number); + mean += all_runs.back(); + } + mean /= repeat; + for (std::size_t r = 0; r < static_cast(repeat); ++r) + { + stdev += (all_runs[r] - mean) * (all_runs[r] - mean); + } + stdev = std::sqrt(stdev / repeat); + + std::cout << _format_time(mean, precision) << " +- " << _format_time(stdev, precision); + std::cout << " per loop (mean +- std. dev. of " << repeat << " run" << ((repeat == 1) ? ", " : "s "); + std::cout << number << " loop" << ((number == 1) ? "" : "s") << " each)" << std::endl; + } +} \ No newline at end of file diff --git a/src/xmagics/execution.hpp b/src/xmagics/execution.hpp new file mode 100644 index 00000000..f2708f75 --- /dev/null +++ b/src/xmagics/execution.hpp @@ -0,0 +1,42 @@ +/************************************************************************************ + * Copyright (c) 2023, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ + +#ifndef XMAGICS_EXECUTION_HPP +#define XMAGICS_EXECUTION_HPP + +#include +#include + +#include "xeus-cpp/xmagics.hpp" +#include "xeus-cpp/xoptions.hpp" + +namespace xcpp +{ + class timeit : public xmagic_line_cell + { + public: + + XEUS_CPP_API + virtual void operator()(const std::string& line) override; + + XEUS_CPP_API + virtual void operator()(const std::string& line, const std::string& cell) override; + + public: + + static int exec_counter; + + private: + + void get_options(argparser& argpars); + std::string inner(std::size_t number, const std::string& code, int exec_counter) const; + std::string _format_time(double timespan, std::size_t precision) const; + void execute(std::string& line, std::string& cell); + }; +} +#endif \ No newline at end of file diff --git a/test/test_interpreter.cpp b/test/test_interpreter.cpp index fd18eb35..5327ce1e 100644 --- a/test/test_interpreter.cpp +++ b/test/test_interpreter.cpp @@ -16,11 +16,12 @@ #include "xeus-cpp/xoptions.hpp" #include "xeus-cpp/xeus_cpp_config.hpp" -#include "../src/xparser.hpp" -#include "../src/xsystem.hpp" +#include "../src/xinspect.hpp" +#include "../src/xmagics/execution.hpp" #include "../src/xmagics/os.hpp" #include "../src/xmagics/xassist.hpp" -#include "../src/xinspect.hpp" +#include "../src/xparser.hpp" +#include "../src/xsystem.hpp" #include @@ -1053,3 +1054,48 @@ TEST_SUITE("file") { infile.close(); } } + +TEST_SUITE("timeit") +{ + TEST_CASE("cell_check") + { + std::string line = "timeit"; + std::string cell = "std::cout << 1 << std::endl;"; + StreamRedirectRAII redirect(std::cout); + xcpp::timeit ti; + ti(line, cell); + std::string output = redirect.getCaptured(); + REQUIRE(output.find("mean +- std. dev. of") != std::string::npos); + } + + TEST_CASE("line_check") + { + std::string line = "timeit std::cout << 1 << std::endl;"; + StreamRedirectRAII redirect(std::cout); + xcpp::timeit ti; + ti(line); + std::string output = redirect.getCaptured(); + REQUIRE(output.find("mean +- std. dev. of") != std::string::npos); + } + + TEST_CASE("arg_check") + { + std::string line = "timeit -n 10 -r 1 -p 6 std::cout << 1 << std::endl;"; + StreamRedirectRAII redirect(std::cout); + xcpp::timeit ti; + ti(line); + std::string output = redirect.getCaptured(); + REQUIRE(output.find("mean +- std. dev. of 1 run, 10 loops each") != std::string::npos); + } + + TEST_CASE("fail_check") + { + std::string line = "timeit"; + std::string cell = "int x = "; + StreamRedirectRAII redirect(std::cerr); + xcpp::timeit ti; + ti(line, cell); + std::string output = redirect.getCaptured(); + REQUIRE(output.find("expected expression") != std::string::npos); + } +}