From ae745ea32a5661ce98e914664af4e916770d799f Mon Sep 17 00:00:00 2001 From: Kirill Mitkin Date: Sun, 19 Jan 2025 15:44:30 +0000 Subject: [PATCH 1/5] add folly_sharedmutex extend switch point insertion fix hook & folly shared mutex try to setup CI to run folly tests refactor folly_rwspinlock add bank && custom blocking primitives format code speed up futex blocking && change release schema forbid CoroYield calls in inappropriate contexts fix CI add flatcombining queue use renaming action clang_pass: support simple names & several match types refactor enable CI fix livelock problem && get rid of legacy token API format code fix pct backoff strategy add ad-hoc termination order refactor verifying targets fix after rebasing onto minimization patch refactor clang pass rewrite totally blocking support at runtime fix livelock avoiding reformat code && enable testing fix formatting scripts && reformat code fix review comments add conditional variables testing add conditional variable primitive fix futex_queues.PopAll pct_strategy: run round robin for livelock avoding add deadlock detection refactor code refactor StrategyVerifier extend buffered channel test --- .github/workflows/clang-format.yaml | 3 +- .github/workflows/run-tests.yaml | 55 +++- .gitignore | 1 - CMakeLists.txt | 11 +- Dockerfile | 17 +- clangpass/CMakeLists.txt | 38 +++ clangpass/ast_consumer.cpp | 71 +++++ clangpass/clangpass_tool.cpp | 107 ++++++++ clangpass/include/clangpass.h | 118 +++++++++ clangpass/refactor_matcher.cpp | 76 ++++++ codegen/coyieldpass.cpp | 6 +- codegen/yieldpass.cpp | 74 +++++- runtime/CMakeLists.txt | 1 + runtime/generators.cpp | 10 - runtime/include/block_manager.h | 49 ++++ runtime/include/block_state.h | 9 + runtime/include/blocking_primitives.h | 87 +++++++ runtime/include/generators.h | 4 - runtime/include/lib.h | 63 ++--- runtime/include/lincheck.h | 3 +- runtime/include/minimization.h | 17 +- runtime/include/minimization_smart.h | 11 +- runtime/include/pct_strategy.h | 194 +++++++------- runtime/include/pick_strategy.h | 73 +----- runtime/include/pretty_print.h | 2 +- runtime/include/random_strategy.h | 28 +- runtime/include/round_robin_strategy.h | 15 +- runtime/include/scheduler.h | 246 ++++++++++++------ runtime/include/scheduler_fwd.h | 14 +- runtime/include/strategy_verifier.h | 11 +- runtime/include/syscall_trap.h | 2 +- runtime/include/value_wrapper.h | 4 +- runtime/include/verifying.h | 29 ++- runtime/include/verifying_macro.h | 32 +-- runtime/include/yield_guard.h | 12 + runtime/lib.cpp | 46 ++-- runtime/minimization.cpp | 20 +- runtime/minimization_smart.cpp | 10 +- runtime/syscall_trap.cpp | 6 +- runtime/verifying.cpp | 17 +- runtime/yield_guard.cpp | 9 + scripts/check.sh | 8 + .../check_ctx_speed.sh | 2 +- scripts/format_code.sh | 2 + syscall_intercept/CMakeLists.txt | 2 +- syscall_intercept/hook.cpp | 86 +++--- test/runtime/lin_check_test.cpp | 4 +- test/runtime/stackfulltask_mock.h | 2 +- third_party/CMakeLists.txt | 2 +- verifying/CMakeLists.txt | 39 ++- verifying/blocking/CMakeLists.txt | 91 +++++-- verifying/blocking/bank.cpp | 106 ++++++++ verifying/blocking/bank_deadlock.cpp | 99 +++++++ verifying/blocking/buffered_channel.cpp | 125 +++++++++ .../blocking/folly_flatcombining_queue.cpp | 34 +++ verifying/blocking/folly_rwspinlock.cpp | 154 +---------- verifying/blocking/folly_sharedmutex.cpp | 30 +++ verifying/blocking/mutexed_register.cpp | 16 +- verifying/blocking/nonlinear_mutex.cpp | 5 +- .../blocking/shared_mutexed_register.cpp | 35 +++ verifying/blocking/simple_deadlock.cpp | 87 +++++++ verifying/blocking/simple_mutex.cpp | 4 +- .../verifiers/buffered_channel_verifier.h | 53 ++++ verifying/blocking/verifiers/mutex_verifier.h | 32 ++- .../verifiers/shared_mutex_verifier.h | 42 +-- verifying/lib/mutex.h | 36 --- verifying/specs/bank.h | 88 +++++++ verifying/specs/bounded_queue.h | 67 ----- verifying/specs/mutex.h | 4 +- verifying/specs/queue.h | 13 +- verifying/specs/register.h | 10 +- verifying/targets/CMakeLists.txt | 6 +- verifying/targets/atomic_register.cpp | 8 +- verifying/targets/counique_args.cpp | 3 +- verifying/targets/deadlock.cpp | 69 ----- verifying/targets/fast_queue.cpp | 18 +- verifying/targets/mutex_queue.cpp | 68 ----- verifying/targets/nonlinear_ms_queue.cpp | 10 +- verifying/targets/nonlinear_queue.cpp | 11 +- verifying/targets/nonlinear_set.cpp | 12 +- verifying/targets/nonlinear_treiber_stack.cpp | 22 +- verifying/targets/race_register.cpp | 4 +- verifying/targets/unique_args.cpp | 2 +- 83 files changed, 2093 insertions(+), 1019 deletions(-) create mode 100644 clangpass/CMakeLists.txt create mode 100644 clangpass/ast_consumer.cpp create mode 100644 clangpass/clangpass_tool.cpp create mode 100644 clangpass/include/clangpass.h create mode 100644 clangpass/refactor_matcher.cpp create mode 100644 runtime/include/block_manager.h create mode 100644 runtime/include/block_state.h create mode 100644 runtime/include/blocking_primitives.h create mode 100644 runtime/include/yield_guard.h create mode 100644 runtime/yield_guard.cpp create mode 100755 scripts/check.sh rename check_ctx_speed.sh => scripts/check_ctx_speed.sh (97%) create mode 100644 verifying/blocking/bank.cpp create mode 100644 verifying/blocking/bank_deadlock.cpp create mode 100644 verifying/blocking/buffered_channel.cpp create mode 100644 verifying/blocking/folly_flatcombining_queue.cpp create mode 100644 verifying/blocking/folly_sharedmutex.cpp create mode 100644 verifying/blocking/shared_mutexed_register.cpp create mode 100644 verifying/blocking/simple_deadlock.cpp create mode 100644 verifying/blocking/verifiers/buffered_channel_verifier.h delete mode 100644 verifying/lib/mutex.h create mode 100644 verifying/specs/bank.h delete mode 100644 verifying/specs/bounded_queue.h delete mode 100644 verifying/targets/deadlock.cpp delete mode 100644 verifying/targets/mutex_queue.cpp diff --git a/.github/workflows/clang-format.yaml b/.github/workflows/clang-format.yaml index 9672fe5d..346de8ca 100644 --- a/.github/workflows/clang-format.yaml +++ b/.github/workflows/clang-format.yaml @@ -8,5 +8,6 @@ jobs: - name: Run clang-format style check uses: jidicula/clang-format-action@v4.13.0 with: - clang-format-version: '14' + clang-format-version: '19' check-path: '.' + fallback-style: 'Google' diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index c036b076..3b561e38 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -9,12 +9,12 @@ jobs: run: shell: bash container: - image: silkeh/clang:18 + image: silkeh/clang:19 options: --user root timeout-minutes: 10 steps: - name: Install deps - run: apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev + run: apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev libclang-19-dev - name: Check out repository code uses: actions/checkout@v4 - name: Build @@ -29,13 +29,13 @@ jobs: run: shell: bash container: - image: silkeh/clang:18 + image: silkeh/clang:19 options: --user root timeout-minutes: 10 steps: - name: Install deps run: | - apt update && apt install -y git ninja-build valgrind libgoogle-glog-dev libsnappy-dev protobuf-compiler libboost-context-dev pkg-config libcapstone-dev && \ + apt update && apt install -y git ninja-build valgrind libgoogle-glog-dev libsnappy-dev protobuf-compiler libboost-context-dev pkg-config libcapstone-dev libclang-19-dev && \ git clone https://github.com/Kirillog/syscall_intercept.git && \ cmake syscall_intercept -G Ninja -B syscall_intercept/build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang && \ cmake --build syscall_intercept/build --target install @@ -46,4 +46,49 @@ jobs: cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=RelWithAssert cmake --build build --target verify-targets verify-blocking - name: "Tests" - run: ctest --test-dir build -L "verify" -V + run: ctest --parallel 4 --test-dir build -L "verify" -V + verifying-folly-release: + runs-on: ubuntu-latest + env: + LTEST_BUILD_PATH: "/__w/LTest/LTest/build" + defaults: + run: + shell: bash + container: + image: silkeh/clang:19 + options: --user root + steps: + - name: Install deps + run: | + apt update && apt install -y git ninja-build valgrind libgoogle-glog-dev libsnappy-dev libclang-19-dev \ + protobuf-compiler libboost-context-dev pkg-config libcapstone-dev \ + libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev \ + libdouble-conversion-dev libfast-float-dev libevent-dev libssl-dev libfmt-dev \ + libgoogle-glog-dev zlib1g-dev && \ + git clone --depth=1 https://github.com/Kirillog/syscall_intercept.git && \ + cmake syscall_intercept -G Ninja -B syscall_intercept/build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang && \ + cmake --build syscall_intercept/build --target install + - name: Check out repository code + uses: actions/checkout@v4 + - name: Build folly ltest dependencies + run: | + cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build --target plugin_pass runtime + - name: Install folly + run: | + git clone --depth=1 https://github.com/Kirillog/folly.git && \ + cmake folly -G Ninja -B folly/build_dir -DCMAKE_BUILD_TYPE=Release && \ + cmake --build folly/build_dir --target install + - name: Build tests + run: | + cmake --build build --target verifying/blocking/folly_rwspinlock verifying/blocking/folly_sharedmutex \ + verifying/blocking/folly_flatcombining_queue + - name: Run folly rwspinlock with pct strategy + run: | + ./scripts/check.sh 0 ./build/verifying/blocking/folly_rwspinlock --strategy pct --rounds 10000 + - name: Run folly shared_mutex with pct strategy + run: | + ./scripts/check.sh 0 ./build/verifying/blocking/folly_sharedmutex --strategy pct --rounds 10000 + - name: Run folly flatcombining queue with pct strategy + run: | + ./scripts/check.sh 0 ./build/verifying/blocking/folly_flatcombining_queue --strategy pct --rounds 10000 diff --git a/.gitignore b/.gitignore index 5094ec8e..cce08864 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ venv third_party/** !third_party/CMakeLists.txt dist -folly \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 4bed1bb7..8234d0eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,10 +7,11 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(APPLY_CLANG_TOOL ON) + # TODO(kmitkin): require to understand, what is it considered to be "optimized" build # set(CMAKE_CXX_FLAGS_RELEASE "???") -set(CMAKE_CXX_FLAGS_DEBUG "-g -ggdb3 -O0 -fno-omit-frame-pointer") -# set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(CMAKE_CONFIGURATION_TYPES "Debug;Release;RelWithAssert" CACHE STRING "" FORCE) @@ -18,18 +19,22 @@ set(CMAKE_CONFIGURATION_TYPES "Debug;Release;RelWithAssert" CACHE STRING "" FORC set(CMAKE_C_FLAGS_RELWITHASSERT "${CMAKE_C_FLAGS_RELEASE} -UNDEBUG" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS_RELWITHASSERT "${CMAKE_CXX_FLAGS_RELEASE} -UNDEBUG" CACHE STRING "" FORCE) + +set(CMAKE_CXX_FLAGS_DEBUG "-DDEBUG -g -ggdb3 -O0 -fno-omit-frame-pointer") set(CMAKE_WARN_FLAGS -Wall -Wextra -Werror -pedantic-errors) + if(CMAKE_BUILD_TYPE MATCHES Debug) message(STATUS "Debug mode ON") set(CMAKE_ASAN_FLAGS -fsanitize=address -fsanitize=undefined -DADDRESS_SANITIZER) endif(CMAKE_BUILD_TYPE MATCHES Debug) add_subdirectory(third_party) - +include(CTest) include(GoogleTest) fuzztest_setup_fuzzing_flags() enable_testing() +add_subdirectory(clangpass) add_subdirectory(codegen) add_subdirectory(runtime) diff --git a/Dockerfile b/Dockerfile index 377dbfd1..3c9ce9c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,26 @@ FROM silkeh/clang:19 AS ltest -RUN apt update && apt install -y git ninja-build valgrind libboost-context-dev libgflags-dev libstdc++-11-dev +RUN apt update && apt install -y git ninja-build valgrind libgflags-dev libstdc++-11-dev libclang-19-dev RUN mv /usr/lib/gcc/x86_64-linux-gnu/12 /usr/lib/gcc/x86_64-linux-gnu/_12 FROM ltest as blocking -RUN apt install -y pkg-config libcapstone-dev && \ +RUN apt install -y pkg-config libcapstone-dev libboost-context-dev && \ git clone https://github.com/Kirillog/syscall_intercept.git && \ cmake syscall_intercept -G Ninja -B syscall_intercept/build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang && \ cmake --build syscall_intercept/build --target install +FROM blocking as folly-blocking +RUN apt install -y libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev \ + libdouble-conversion-dev libfast-float-dev libevent-dev libssl-dev libfmt-dev \ + libgoogle-glog-dev zlib1g-dev && \ + git clone https://github.com/Kirillog/folly.git && \ + cmake folly -G Ninja -B folly/build_dir -DCMAKE_BUILD_TYPE=Release + # cmake --build folly/build_dir --target install +FROM ltest as userver-blocking +# userver conflicts with default libboost-context-dev (1.74) version, 1.81 required +RUN apt install -y python3-dev python3-venv \ + libboost-context1.81-dev libboost-filesystem1.81-dev libboost-program-options1.81-dev libboost-regex1.81-dev + libboost-stacktrace1.81-dev libboost-locale1.81-dev \ + libzstd-dev libyaml-cpp-dev libcrypto++-dev libnghttp2-dev libev-dev RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.1/zsh-in-docker.sh)" -- \ -p git CMD [ "zsh" ] diff --git a/clangpass/CMakeLists.txt b/clangpass/CMakeLists.txt new file mode 100644 index 00000000..089af45e --- /dev/null +++ b/clangpass/CMakeLists.txt @@ -0,0 +1,38 @@ +#=============================================================================== +# SETUP CLANG PLUGIN +#=============================================================================== +find_package(Clang REQUIRED CONFIG) +if("${LLVM_VERSION_MAJOR}" VERSION_LESS 19) + message(FATAL_ERROR "Found LLVM ${LLVM_VERSION_MAJOR}, but need LLVM 19 or above") +endif() + +include_directories(SYSTEM "${LLVM_INCLUDE_DIRS};${CLANG_INCLUDE_DIRS}") +#=============================================================================== +# SETUP CLANG TOOL +#=============================================================================== +set(CLANG_TOOL "ClangPassTool") +set(CLANG_TOOL_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/clangpass_tool.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ast_consumer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/refactor_matcher.cpp" +) + +add_executable( + ${CLANG_TOOL} + ${CLANG_TOOL_SOURCES} +) + +# Configure include directories for 'tool' +target_include_directories( + ${CLANG_TOOL} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/include" +) + +# Link in the required libraries +target_link_libraries( + ${CLANG_TOOL} + PRIVATE + clangTooling + clangToolingRefactoring +) \ No newline at end of file diff --git a/clangpass/ast_consumer.cpp b/clangpass/ast_consumer.cpp new file mode 100644 index 00000000..000a62e8 --- /dev/null +++ b/clangpass/ast_consumer.cpp @@ -0,0 +1,71 @@ + +#include "include/clangpass.h" + +replace_pass::CodeRefactorASTConsumer::CodeRefactorASTConsumer( + ASTContext& context, clang::Rewriter& rewriter, + std::vector names, std::string temp_prefix) + : refactor_handler(context, rewriter, names), + names(names), + rewriter(rewriter), + temp_prefix(std::move(temp_prefix)) { + auto factory = NameMatcherFactory(); + + for (auto name : names) { + const auto elaborated_matcher = factory.CreateMatcherFor(name.old_name); + // Uses previous matcher inside, but returns a wrapping QualifiedTypeLoc + // node which is used in the function parameters + const auto qualified_matcher = + qualifiedTypeLoc(hasUnqualifiedLoc(elaborated_matcher)); + match_finder.addMatcher( + elaborated_matcher.bind("ElaboratedTypeLoc" + name.old_name), + &refactor_handler); + match_finder.addMatcher( + qualified_matcher.bind("QualifiedTypeLoc" + name.old_name), + &refactor_handler); + } +} + +std::string replace_pass::CodeRefactorASTConsumer::RefactoredFileName( + StringRef original_filename) const { + size_t slash_index = original_filename.rfind("/"); + // the path should be absolute, so in the worst case we will get '/' as index + // 0 + assert(slash_index != std::string::npos); + slash_index += 1; // include the '/' itself + + std::string path = std::string(original_filename.begin(), + original_filename.begin() + slash_index); + std::string filename = std::string(original_filename.begin() + slash_index, + original_filename.end()); + + return path + temp_prefix + filename; +} + +void replace_pass::CodeRefactorASTConsumer::HandleTranslationUnit( + clang::ASTContext& ctx) { + match_finder.matchAST(ctx); + const SourceManager& source_manager = rewriter.getSourceMgr(); + auto fileID = source_manager.getMainFileID(); + const RewriteBuffer& buffer = rewriter.getEditBuffer(fileID); + + // Output to stdout + llvm::outs() << "Transformed code:\n"; + buffer.write(llvm::outs()); + llvm::outs() << "\n"; + + // Output to file + const FileEntry* entry = source_manager.getFileEntryForID(fileID); + std::string new_filename = RefactoredFileName(entry->tryGetRealPathName()); + + std::error_code code; + llvm::raw_fd_ostream ostream(new_filename, code, llvm::sys::fs::OF_None); + if (code) { + llvm::errs() << "Error: Could not open output file: " << code.message() + << "\n"; + return; + } + + llvm::outs() << "Writing to file: " << new_filename << "\n"; + buffer.write(ostream); + ostream.close(); +} diff --git a/clangpass/clangpass_tool.cpp b/clangpass/clangpass_tool.cpp new file mode 100644 index 00000000..c04fcd1d --- /dev/null +++ b/clangpass/clangpass_tool.cpp @@ -0,0 +1,107 @@ +//============================================================================== +// FILE: +// clangpass_tool.cpp +// +// DESCRIPTION: +// Replaces all ::std:: names usages to custom runtime friendly +// implementations +// +// USAGE: +// * ./build/bin/clangpass_tool --replace-names=::std::atomic,::std::mutex +// --insert-names=LTestAtomic,ltest::mutex +// ./verifying/targets/nonlinear_queue.cpp +// +// License: The Unlicense +//============================================================================== +#include +#include +#include +#include + +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Tooling/CommonOptionsParser.h" +#include "clang/Tooling/Refactoring.h" +#include "include/clangpass.h" +#include "llvm/Support/CommandLine.h" + +using namespace clang; +using namespace replace_pass; + +//===----------------------------------------------------------------------===// +// Command line options +//===----------------------------------------------------------------------===// +static cl::OptionCategory CodeRefactorCategory("atomics-replacer options"); + +static cl::opt TemporaryPrefix{ + "temp-prefix", cl::desc("Prefix for temporary files"), cl::init("__tmp_"), + cl::cat(CodeRefactorCategory)}; +static cl::list ClassNamesToReplace{ + "replace-names", + cl::desc("Names of the classes/structs which usages should be renamed"), + cl::OneOrMore, cl::CommaSeparated, cl::cat(CodeRefactorCategory)}; +static cl::list ClassNamesToInsert{ + "insert-names", + cl::desc("Names of the classes/structs which should be used instead"), + cl::OneOrMore, cl::CommaSeparated, cl::cat(CodeRefactorCategory)}; + +//===----------------------------------------------------------------------===// +// PluginASTAction +//===----------------------------------------------------------------------===// +class CodeRefactorPluginAction : public PluginASTAction { + public: + explicit CodeRefactorPluginAction() {} + // Not used + bool ParseArgs(const CompilerInstance &compiler, + const std::vector &args) override { + return true; + } + + std::unique_ptr CreateASTConsumer(CompilerInstance &compiler, + StringRef file) override { + rewriter.setSourceMgr(compiler.getSourceManager(), compiler.getLangOpts()); + + std::vector pairs; + pairs.reserve(ClassNamesToReplace.size()); + for (int i = 0; i < ClassNamesToReplace.size(); ++i) { + pairs.emplace_back(ClassNamesToReplace[i], ClassNamesToInsert[i]); + } + + return std::make_unique( + compiler.getASTContext(), rewriter, pairs, TemporaryPrefix); + } + + private: + Rewriter rewriter; +}; + +//===----------------------------------------------------------------------===// +// Main driver code. +//===----------------------------------------------------------------------===// +int main(int argc, const char **argv) { + Expected options = + clang::tooling::CommonOptionsParser::create(argc, argv, + CodeRefactorCategory); + if (auto E = options.takeError()) { + errs() << "Problem constructing CommonOptionsParser " + << toString(std::move(E)) << '\n'; + return EXIT_FAILURE; + } + + // auto files = eOptParser->getSourcePathList(); + // std::vector Args = {"clang++", "-E" }; + // Args.insert(Args.end(), files.begin(), files.end()); + + // auto OptionsParser = clang::tooling::CommonOptionsParser::create( + // Args.size(), Args.data(), + // ); + + // clang::tooling::ClangTool Tool(OptionsParser->getCompilations(), + // OptionsParser->getSourcePathList()); + + clang::tooling::RefactoringTool tool(options->getCompilations(), + options->getSourcePathList()); + + return tool.run( + clang::tooling::newFrontendActionFactory() + .get()); +} diff --git a/clangpass/include/clangpass.h b/clangpass/include/clangpass.h new file mode 100644 index 00000000..3735c263 --- /dev/null +++ b/clangpass/include/clangpass.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "clang/AST/ASTConsumer.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Rewrite/Core/Rewriter.h" + +using namespace clang; +using namespace ast_matchers; +using namespace llvm; + +namespace replace_pass { + +struct TypeName { + enum : int { TemplateName, SimpleName }; + + static int of(std::string_view name) { + if (name == "::std::mutex" || name == "::std::shared_mutex" || + name == "::std::condition_variable") { + return TypeName::SimpleName; + } else if (name == "::std::atomic") { + return TypeName::TemplateName; + } else { + assert(false); + } + } +}; + +struct ReplacePair { + std::string old_name; + std::string new_name; +}; + +//----------------------------------------------------------------------------- +// ASTFinder callback +//----------------------------------------------------------------------------- +class CodeRefactorMatcher + : public clang::ast_matchers::MatchFinder::MatchCallback { + public: + using MatchResult = clang::ast_matchers::MatchFinder::MatchResult; + + explicit CodeRefactorMatcher(ASTContext &context, clang::Rewriter &rewriter, + std::vector names); + + void run(const MatchResult &result) override; + std::string GetArgumentsFromTemplateType( + const TemplateSpecializationType *type); + + private: + void runFor(const ReplacePair &p, const MatchResult &result); + + private: + ASTContext &context; + clang::Rewriter &rewriter; + std::vector names; +}; + +//----------------------------------------------------------------------------- +// ASTConsumer +//----------------------------------------------------------------------------- +class CodeRefactorASTConsumer : public clang::ASTConsumer { + public: + CodeRefactorASTConsumer(ASTContext &context, clang::Rewriter &rewriter, + std::vector names, + std::string temp_prefix); + + void HandleTranslationUnit(clang::ASTContext &ctx) override; + + private: + std::string RefactoredFileName(StringRef original_filename) const; + + private: + clang::ast_matchers::MatchFinder match_finder; + CodeRefactorMatcher refactor_handler; + clang::Rewriter &rewriter; + + std::vector names; + std::string temp_prefix; +}; + +class NameMatcherFactory { + using ClangMatcher = + ::clang::ast_matchers::internal::BindableMatcher; + + public: + NameMatcherFactory() {} + + // Does not support matching the parameters of the functions + ClangMatcher CreateMatcherFor(std::string_view name) { + switch (TypeName::of(name)) { + case TypeName::SimpleName: { + return CreateSimpleMatcherFor(name); + } + case TypeName::TemplateName: { + return CreateTemplateMatcherFor(name); + } + default: + assert(false && "Unsupported type name"); + }; + } + + ClangMatcher CreateSimpleMatcherFor(std::string_view name) { + return elaboratedTypeLoc(hasNamedTypeLoc( + internal::BindableMatcher(new internal::TypeLocTypeMatcher( + (hasDeclaration(cxxRecordDecl(hasName(name)))))))); + } + + ClangMatcher CreateTemplateMatcherFor(std::string_view name) { + return elaboratedTypeLoc(hasNamedTypeLoc(loc(templateSpecializationType( + hasDeclaration(classTemplateSpecializationDecl(hasName(name))))))); + } +}; +} // namespace replace_pass \ No newline at end of file diff --git a/clangpass/refactor_matcher.cpp b/clangpass/refactor_matcher.cpp new file mode 100644 index 00000000..f745b4b3 --- /dev/null +++ b/clangpass/refactor_matcher.cpp @@ -0,0 +1,76 @@ +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/Expr.h" +#include "clang/AST/ExprCXX.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Rewrite/Core/Rewriter.h" +#include "clangpass.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace ast_matchers; +using namespace replace_pass; + +//----------------------------------------------------------------------------- +// ASTFinder callback +//----------------------------------------------------------------------------- +CodeRefactorMatcher::CodeRefactorMatcher(ASTContext& context, + clang::Rewriter& rewriter, + std::vector names) + : context(context), rewriter(rewriter), names(names) {} + +void CodeRefactorMatcher::runFor(const ReplacePair& p, + const MatchResult& result) { + auto type = TypeName::of(p.old_name); + switch (type) { + case TypeName::SimpleName: { + if (const auto* ETL = result.Nodes.getNodeAs( + "ElaboratedTypeLoc" + p.old_name)) { + rewriter.ReplaceText(ETL->getSourceRange(), p.new_name); + } + if (const auto* QTL = result.Nodes.getNodeAs( + "QualifiedTypeLoc" + p.old_name)) { + rewriter.ReplaceText(QTL->getSourceRange(), p.new_name); + } + } + case TypeName::TemplateName: { + if (const auto* ETL = result.Nodes.getNodeAs( + "ElaboratedTypeLoc" + p.old_name)) { + const auto* TemplType = + ETL->getType()->getAs(); + if (!TemplType) { + return; + } + rewriter.ReplaceText( + ETL->getSourceRange(), + p.new_name + GetArgumentsFromTemplateType(TemplType)); + } + if (const auto* QTL = result.Nodes.getNodeAs( + "QualifiedTypeLoc" + p.old_name)) { + const auto* TemplType = + QTL->getType()->getAs(); + if (!TemplType) { + return; + } + rewriter.ReplaceText( + QTL->getSourceRange(), + p.new_name + GetArgumentsFromTemplateType(TemplType)); + } + } + } +} + +void CodeRefactorMatcher::run(const MatchResult& result) { + for (size_t i = 0; i < names.size(); ++i) { + runFor(names[i], result); + } +} + +std::string CodeRefactorMatcher::GetArgumentsFromTemplateType( + const TemplateSpecializationType* type) { + std::string args; + llvm::raw_string_ostream os(args); + printTemplateArgumentList(os, type->template_arguments(), + context.getPrintingPolicy()); + return args; +} diff --git a/codegen/coyieldpass.cpp b/codegen/coyieldpass.cpp index d97589aa..229f3490 100644 --- a/codegen/coyieldpass.cpp +++ b/codegen/coyieldpass.cpp @@ -136,9 +136,11 @@ struct CoYieldInserter { InsertCall(filt_entry, builder, true); // Invoke instruction has unwind/normal ends so we need handle it if (invoke) { - builder.SetInsertPoint(invoke->getNormalDest()->getFirstInsertionPt()); + builder.SetInsertPoint( + invoke->getNormalDest()->getFirstInsertionPt()); InsertCall(filt_entry, builder, false); - builder.SetInsertPoint(invoke->getUnwindDest()->getFirstInsertionPt()); + builder.SetInsertPoint( + invoke->getUnwindDest()->getFirstInsertionPt()); InsertCall(filt_entry, builder, false); } else { builder.SetInsertPoint(call->getNextNode()); diff --git a/codegen/yieldpass.cpp b/codegen/yieldpass.cpp index 51ad0873..2b495f39 100644 --- a/codegen/yieldpass.cpp +++ b/codegen/yieldpass.cpp @@ -10,6 +10,7 @@ using Builder = IRBuilder<>; using FunIndex = std::set>; const StringRef nonatomic_attr = "ltest_nonatomic"; +const StringRef atomic_attr = "ltest_atomic"; FunIndex CreateFunIndex(const Module &M) { FunIndex index{}; @@ -44,20 +45,27 @@ struct YieldInserter { void Run(const FunIndex &index) { for (auto &F : M) { - if (IsTarget(F.getName(), index)) { - InsertYields(F, index); + if (IsAtomic(F.getName(), index)) { + CollectAtomic(F, index); + } + } - errs() << "yields inserted to the " << F.getName() << "\n"; - errs() << F << "\n"; + for (auto &F : M) { + if (IsNonAtomic(F.getName(), index)) { + InsertYields(F, index); } } } private: - bool IsTarget(const StringRef fun_name, const FunIndex &index) { + bool IsNonAtomic(const StringRef fun_name, const FunIndex &index) { return HasAttribute(index, fun_name, nonatomic_attr); } + bool IsAtomic(const StringRef fun_name, const FunIndex &index) { + return HasAttribute(index, fun_name, atomic_attr); + } + bool NeedInterrupt(Instruction *insn, const FunIndex &index) { if (isa(insn) || isa(insn) || isa(insn) /*|| @@ -67,7 +75,39 @@ struct YieldInserter { return false; } + void CollectAtomic(Function &F, const FunIndex &index) { + auto name = F.getName(); + if (atomic.find(name) != atomic.end()) { + return; + } + atomic.insert(name); + for (auto &B : F) { + for (auto &I : B) { + if (auto call = dyn_cast(&I)) { + auto fun = call->getCalledFunction(); + if (fun && !fun->isDeclaration()) { + CollectAtomic(*fun, index); + } + } + if (auto invoke = dyn_cast(&I)) { + auto fun = invoke->getCalledFunction(); + if (fun && !fun->isDeclaration()) { + CollectAtomic(*fun, index); + } + } + } + } + } + void InsertYields(Function &F, const FunIndex &index) { + auto name = F.getName(); + if (visited.find(name) != visited.end() || + atomic.find(name) != atomic.end()) { + return; + } + + visited.insert(name); + Builder Builder(&*F.begin()); for (auto &B : F) { for (auto it = B.begin(); std::next(it) != B.end(); ++it) { @@ -78,6 +118,28 @@ struct YieldInserter { } } } + +#ifndef DEBUG + for (auto &B : F) { + for (auto &I : B) { + if (auto call = dyn_cast(&I)) { + auto fun = call->getCalledFunction(); + if (fun && !fun->isDeclaration()) { + InsertYields(*fun, index); + } + } + if (auto invoke = dyn_cast(&I)) { + auto fun = invoke->getCalledFunction(); + if (fun && !fun->isDeclaration()) { + InsertYields(*fun, index); + } + } + } + } +#endif + + errs() << "yields inserted to the " << F.getName() << "\n"; + errs() << F << "\n"; } bool ItsYieldInst(Instruction *inst) { @@ -94,6 +156,8 @@ struct YieldInserter { Module &M; FunctionCallee CoroYieldF; + std::set visited{}; + std::set atomic{}; }; namespace { diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 895cb274..61d04054 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -9,6 +9,7 @@ set (SOURCE_FILES syscall_trap.cpp minimization.cpp minimization_smart.cpp + yield_guard.cpp ) add_library(runtime SHARED ${SOURCE_FILES}) diff --git a/runtime/generators.cpp b/runtime/generators.cpp index f4049c69..e55a1433 100644 --- a/runtime/generators.cpp +++ b/runtime/generators.cpp @@ -4,19 +4,9 @@ namespace ltest { namespace generators { -std::shared_ptr generated_token{}; - // Generates empty arguments. std::tuple<> genEmpty(size_t thread_num) { return std::tuple<>(); } -// Generates runtime token. -// Can be called only once per task creation. -std::tuple> genToken(size_t thread_num) { - assert(!generated_token && "forgot to reset generated_token"); - generated_token = std::make_shared(); - return {generated_token}; -} - } // namespace generators } // namespace ltest diff --git a/runtime/include/block_manager.h b/runtime/include/block_manager.h new file mode 100644 index 00000000..0c348528 --- /dev/null +++ b/runtime/include/block_manager.h @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include + +#include "block_state.h" + +struct CoroBase; + +struct BlockManager { + // TODO(kmitkin): due to usage in as_atomic functions rewrite to custom hash + // table & linked list + std::unordered_map> queues; + + void BlockOn(BlockState state, CoroBase *coro) { + if (!queues.contains(state.addr)) { + queues[state.addr] = std::deque{}; + } + queues[state.addr].push_back(coro); + } + + bool IsBlocked(const BlockState &state, CoroBase *coro) { + return state.addr && + std::find(queues[state.addr].begin(), queues[state.addr].end(), + coro) != queues[state.addr].end(); + } + + std::size_t UnblockOn(std::intptr_t addr, std::size_t max_wakes) { + if (!queues.contains(addr)) [[unlikely]] { + return 0; + } + auto &queue = queues[addr]; + size_t wakes = 0; + for (; wakes < max_wakes && !queue.empty(); ++wakes) { + queue.pop_front(); // Can be spurious wake ups + } + return wakes; + } + + void UnblockAllOn(std::intptr_t addr) { + if (!queues.contains(addr)) { + return; + } + queues[addr].clear(); + } +}; + +extern BlockManager block_manager; diff --git a/runtime/include/block_state.h b/runtime/include/block_state.h new file mode 100644 index 00000000..cbd599bb --- /dev/null +++ b/runtime/include/block_state.h @@ -0,0 +1,9 @@ +#pragma once + +#include +struct BlockState { + std::intptr_t addr; + long value; + + inline bool CanBeBlocked() { return *reinterpret_cast(addr) == value; } +}; \ No newline at end of file diff --git a/runtime/include/blocking_primitives.h b/runtime/include/blocking_primitives.h new file mode 100644 index 00000000..d439b848 --- /dev/null +++ b/runtime/include/blocking_primitives.h @@ -0,0 +1,87 @@ +#pragma once +#include + +#include "block_manager.h" +#include "lib.h" +#include "verifying_macro.h" + +namespace ltest { + +struct mutex { + as_atomic void lock() { + while (locked) { + this_coro->SetBlocked(state); + CoroYield(); + } + locked = 1; + } + + as_atomic bool try_lock() { + if (locked) { + CoroYield(); + return false; + } + locked = 1; + return true; + } + + as_atomic void unlock() { + locked = 0; + block_manager.UnblockAllOn( + state.addr); // To have the ability schedule any coroutine + } + + private: + int locked{0}; + BlockState state{reinterpret_cast(&locked), locked}; + + friend struct condition_variable; +}; + +struct shared_mutex { + as_atomic void lock() { + while (locked != 0) { + this_coro->SetBlocked(state); + CoroYield(); + } + locked = -1; + } + as_atomic void unlock() { + locked = 0; + block_manager.UnblockAllOn(state.addr); + } + as_atomic void lock_shared() { + while (locked == -1) { + this_coro->SetBlocked(state); + CoroYield(); + } + ++locked; + } + as_atomic void unlock_shared() { + --locked; + block_manager.UnblockAllOn(state.addr); + } + + private: + int locked{0}; + BlockState state{reinterpret_cast(&locked), locked}; +}; + +struct condition_variable { + as_atomic void wait(std::unique_lock& lock) { + addr = lock.mutex()->state.addr; + lock.unlock(); + this_coro->SetBlocked({addr, 1}); + CoroYield(); + lock.lock(); + } + + as_atomic void notify_one() { block_manager.UnblockOn(addr, 1); } + + as_atomic void notify_all() { block_manager.UnblockAllOn(addr); } + + private: + std::intptr_t addr; +}; + +} // namespace ltest diff --git a/runtime/include/generators.h b/runtime/include/generators.h index 15188c33..a2d7e6a2 100644 --- a/runtime/include/generators.h +++ b/runtime/include/generators.h @@ -8,8 +8,6 @@ namespace ltest { namespace generators { -extern std::shared_ptr generated_token; - // Makes single argument from the value. template auto makeSingleArg(T&& arg) { @@ -19,8 +17,6 @@ auto makeSingleArg(T&& arg) { std::tuple<> genEmpty(size_t thread_num); -std::tuple> genToken(size_t thread_num); - } // namespace generators } // namespace ltest diff --git a/runtime/include/lib.h b/runtime/include/lib.h index b629c970..5901e049 100644 --- a/runtime/include/lib.h +++ b/runtime/include/lib.h @@ -1,6 +1,4 @@ #pragma once -#include - #include #include #include @@ -12,6 +10,7 @@ #include #include +#include "block_manager.h" #include "value_wrapper.h" #define panic() assert(false) @@ -19,35 +18,23 @@ struct CoroBase; struct CoroutineStatus; +struct BlockManager; + // Current executing coroutine. extern std::shared_ptr this_coro; +// Scheduler context extern boost::context::fiber_context sched_ctx; extern std::optional coroutine_status; -struct CoroutineStatus{ +extern BlockManager block_manager; + +struct CoroutineStatus { std::string_view name; bool has_started; }; -// Runtime token. -// Target method could use token generator. -struct Token { - // Parks the task. Yields. - void Park(); - // Unpark the task parked by token. - void Unpark(); - - private: - // Resets the token. - void Reset(); - // If token is parked. - bool parked{}; - - friend class CoroBase; -}; - extern "C" void CoroYield(); extern "C" void CoroutineStatusChange(char* coroutine, bool start); @@ -86,35 +73,28 @@ struct CoroBase : public std::enable_shared_from_this { // https://en.cppreference.com/w/cpp/memory/enable_shared_from_this std::shared_ptr GetPtr(); + // Try to terminate the coroutine. + void TryTerminate(); + // Terminate the coroutine. void Terminate(); - // Sets the token. - void SetToken(std::shared_ptr); - - struct FutexState { - int* addr; - int value; - }; - - inline void SetBlocked(long uaddr, int value) { - fstate = {reinterpret_cast(uaddr), value}; + void SetBlocked(const BlockState& state) { + fstate = state; + block_manager.BlockOn(state, this); } - inline bool IsBlocked() { - /// Check that value stored by futex addr isn't changed - bool is_blocked = fstate.addr && *fstate.addr == fstate.value; - if (!is_blocked) { - fstate = FutexState{nullptr, 0}; - } - return is_blocked; - } + BlockState GetBlockState() { return fstate; } + + bool IsBlocked() { return block_manager.IsBlocked(fstate, this); } // Checks if the coroutine is parked. bool IsParked() const; virtual ~CoroBase(); + boost::context::fiber_context& GetCtx() { return ctx; } + protected: CoroBase() = default; @@ -131,11 +111,9 @@ struct CoroBase : public std::enable_shared_from_this { // Is coroutine returned. bool is_returned{}; // Futex state on which coroutine is blocked. - FutexState fstate{}; + BlockState fstate{}; // Name. std::string_view name; - // Token. - std::shared_ptr token{}; boost::context::fiber_context ctx; }; @@ -157,9 +135,6 @@ struct Coro final : public CoroBase { */ assert(IsReturned()); auto coro = New(func, this_ptr, args, args_to_strings, name, id); - if (token != nullptr) { - coro->token = std::move(token); - } return coro; } diff --git a/runtime/include/lincheck.h b/runtime/include/lincheck.h index 178fd3d5..af01a1e6 100644 --- a/runtime/include/lincheck.h +++ b/runtime/include/lincheck.h @@ -167,7 +167,8 @@ bool LinearizabilityChecker< bool was_checked = false; LinearSpecificationObject data_structure_state_copy = data_structure_state; - ValueWrapper res = method(&data_structure_state_copy, inv.GetTask()->GetArgs()); + ValueWrapper res = + method(&data_structure_state_copy, inv.GetTask()->GetArgs()); // If invoke doesn't have a response we can't check the response bool doesnt_have_response = diff --git a/runtime/include/minimization.h b/runtime/include/minimization.h index 45039ce4..2497f2c7 100644 --- a/runtime/include/minimization.h +++ b/runtime/include/minimization.h @@ -1,7 +1,6 @@ #pragma once #include -#include "lib.h" #include "scheduler_fwd.h" /** @@ -11,8 +10,9 @@ struct RoundMinimizor { // Minimizes the number of tasks in the nonlinearized history; modifies // argument `nonlinear_history`. - virtual void Minimize(SchedulerWithReplay& sched, - Scheduler::BothHistories& nonlinear_history) const = 0; + virtual void Minimize( + SchedulerWithReplay& sched, + Scheduler::NonLinearizableHistory& nonlinear_history) const = 0; /** * Returns ids of tasks taken from `full_history`, excluding those ids, that @@ -37,8 +37,9 @@ struct GreedyRoundMinimizor : public RoundMinimizor { * pairs of tasks and remove them together, this is done to account for * data-structures that have the `add/remove` semantics. */ - void Minimize(SchedulerWithReplay& sched, - Scheduler::BothHistories& nonlinear_history) const override; + void Minimize( + SchedulerWithReplay& sched, + Scheduler::NonLinearizableHistory& nonlinear_history) const override; protected: /** @@ -54,7 +55,7 @@ struct GreedyRoundMinimizor : public RoundMinimizor { */ virtual Scheduler::Result OnTasksRemoved( SchedulerWithReplay& sched, - const Scheduler::BothHistories& nonlinear_history, + const Scheduler::NonLinearizableHistory& nonlinear_history, const std::unordered_set& task_ids) const = 0; }; @@ -71,7 +72,7 @@ struct SameInterleavingMinimizor : public GreedyRoundMinimizor { protected: virtual Scheduler::Result OnTasksRemoved( SchedulerWithReplay& sched, - const Scheduler::BothHistories& nonlinear_history, + const Scheduler::NonLinearizableHistory& nonlinear_history, const std::unordered_set& task_ids) const override; }; @@ -92,7 +93,7 @@ struct StrategyExplorationMinimizor : public GreedyRoundMinimizor { protected: virtual Scheduler::Result OnTasksRemoved( SchedulerWithReplay& sched, - const Scheduler::BothHistories& nonlinear_history, + const Scheduler::NonLinearizableHistory& nonlinear_history, const std::unordered_set& task_ids) const override; private: diff --git a/runtime/include/minimization_smart.h b/runtime/include/minimization_smart.h index 016fed8d..3ca3c6ee 100644 --- a/runtime/include/minimization_smart.h +++ b/runtime/include/minimization_smart.h @@ -2,11 +2,9 @@ #include #include -#include #include #include -#include "lib.h" #include "minimization.h" #include "pretty_print.h" #include "scheduler_fwd.h" @@ -37,13 +35,14 @@ struct SmartMinimizor : public RoundMinimizor { int offsprings_generation_attemps = 10, int initial_mutations_count = 10); - void Minimize(SchedulerWithReplay& sched, - Scheduler::BothHistories& nonlinear_histories) const override; + void Minimize( + SchedulerWithReplay& sched, + Scheduler::NonLinearizableHistory& nonlinear_histories) const override; private: struct Solution { explicit Solution(const Strategy& strategy, - const Scheduler::BothHistories& histories, + const Scheduler::NonLinearizableHistory& histories, int total_tasks); float GetFitness() const; @@ -51,7 +50,7 @@ struct SmartMinimizor : public RoundMinimizor { std::unordered_map> tasks; // ThreadId -> { ValidTaskId1, ValidTaskId2, ... } - Scheduler::BothHistories nonlinear_histories; + Scheduler::NonLinearizableHistory nonlinear_histories; // Fitness is a value in range [0.0, 1.0], the bigger it is, the better is // the Solution. private: diff --git a/runtime/include/pct_strategy.h b/runtime/include/pct_strategy.h index d212d78b..a3dd8e82 100644 --- a/runtime/include/pct_strategy.h +++ b/runtime/include/pct_strategy.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "scheduler.h" @@ -9,53 +11,27 @@ // K represents the maximal number of potential switches in the program // Although it's impossible to predict the exact number of switches(since it's // equivalent to the halt problem), k should be good approximation -template +template struct PctStrategy : public BaseStrategyWithThreads { - // forbid_all_same indicates whether it is allowed to have all same tasks(same - // methods) in any moment of execution. Useful for blocking structures, for - // instance, you don't want to run mutex.lock in each thread - explicit PctStrategy(size_t threads_count, - std::vector constructors, - bool forbid_all_same) - : threads_count(threads_count), + PctStrategy(size_t threads_count, std::vector ctrs) + : BaseStrategyWithThreads(threads_count, ctrs), current_depth(1), - current_schedule_length(0), - forbid_all_same(forbid_all_same) { - this->constructors = std::move(constructors); - this->round_schedule.resize(threads_count, -1); - - std::random_device dev; - rng = std::mt19937(dev()); - this->constructors_distribution = - std::uniform_int_distribution( - 0, this->constructors.size() - 1); - - // We have information about potential number of resumes - // but because of the implementation, it's only available in the task. - // In fact, it doesn't depend on the task, it only depends on the - // constructor - size_t avg_k = 0; - avg_k = avg_k / this->constructors.size(); - - PrepareForDepth(current_depth, avg_k); - - // Create queues. - for (size_t i = 0; i < threads_count; ++i) { - this->threads.emplace_back(); - } + current_schedule_length(0) { + PrepareForDepth(current_depth, 1); } - // If there aren't any non returned tasks and the amount of finished tasks - // is equal to the max_tasks the finished task will be returned - TaskWithMetaData Next() override { + std::optional NextThreadId() override { auto& threads = this->threads; size_t max = std::numeric_limits::min(); size_t index_of_max = 0; // Have to ignore waiting threads, so can't do it faster than O(n) for (size_t i = 0; i < threads.size(); ++i) { // Ignore waiting tasks - if (!threads[i].empty() && - (threads[i].back()->IsParked() || threads[i].back()->IsBlocked())) { + // debug(stderr, "prior: %d, number %d\n", priorities[i], i); + if (!threads[i].empty() && threads[i].back()->IsBlocked()) { + debug(stderr, "blocked on %p val %d\n", + threads[i].back()->GetBlockState().addr, + threads[i].back()->GetBlockState().value); // dual waiting if request finished, but follow up isn't // skip dual tasks that already have finished the request // section(follow-up will be executed in another task, so we can't @@ -69,8 +45,37 @@ struct PctStrategy : public BaseStrategyWithThreads { } } - assert((max != std::numeric_limits::min() && - "all threads are empty or parked")); + if (round_robin_stage > 0) [[unlikely]] { + for (size_t attempt = 0; attempt < threads.size(); ++attempt) { + auto i = (++last_chosen) % threads.size(); + if (!threads[i].empty() && threads[i].back()->IsBlocked()) { + continue; + } + index_of_max = i; + max = priorities[i]; + break; + } + // debug(stderr, "round robin choose: %d\n", index_of_max); + if (round_robin_start == index_of_max) { + --round_robin_stage; + } + } + + // TODO: Choose wiser constant + if (count_chosen_same == 1000 && index_of_max == last_chosen) [[unlikely]] { + round_robin_stage = 5; + round_robin_start = index_of_max; + } + + if (index_of_max == last_chosen) { + ++count_chosen_same; + } else { + count_chosen_same = 1; + } + + if (max == std::numeric_limits::min()) [[unlikely]] { + return std::nullopt; + } // Check whether the priority change is required current_schedule_length++; @@ -80,38 +85,17 @@ struct PctStrategy : public BaseStrategyWithThreads { } } - if (threads[index_of_max].empty() || - threads[index_of_max].back()->IsReturned()) { - auto constructor = - this->constructors.at(this->constructors_distribution(rng)); - if (forbid_all_same) { - auto names = CountNames(index_of_max); - // TODO: выглядит непонятно и так себе - while (true) { - auto name = constructor.GetName(); - names.insert(name); - CreatedTaskMetaData task = {name, true, index_of_max}; - if (names.size() == 1 || !this->sched_checker.Verify(task)) { - constructor = - this->constructors.at(this->constructors_distribution(rng)); - } else { - break; - } - } - } - - threads[index_of_max].emplace_back( - constructor.Build(&this->state, index_of_max, this->new_task_id++)); - return {threads[index_of_max].back(), true, index_of_max}; - } - - return {threads[index_of_max].back(), false, index_of_max}; + // debug(stderr, "Chosen thread: %d, cnt_count: %d\n", index_of_max, + // count_chosen_same); + last_chosen = index_of_max; + return index_of_max; } - TaskWithMetaData NextSchedule() override { + // NOTE: `Next` version use heuristics for livelock avoiding, but not there + // refactor later to avoid copy-paste + std::optional NextSchedule() override { auto& round_schedule = this->round_schedule; auto& threads = this->threads; - size_t max = std::numeric_limits::min(); size_t index_of_max = 0; // Have to ignore waiting threads, so can't do it faster than O(n) @@ -119,7 +103,7 @@ struct PctStrategy : public BaseStrategyWithThreads { int task_index = this->GetNextTaskInThread(i); // Ignore waiting tasks if (task_index == threads[i].size() || - threads[i][task_index]->IsParked()) { + threads[i][task_index]->IsBlocked()) { // dual waiting if request finished, but follow up isn't // skip dual tasks that already have finished the request // section(follow-up will be executed in another task, so we can't @@ -132,14 +116,42 @@ struct PctStrategy : public BaseStrategyWithThreads { index_of_max = i; } } - // Check whether the priority change is required - current_schedule_length++; - for (size_t i = 0; i < priority_change_points.size(); ++i) { - if (current_schedule_length == priority_change_points[i]) { - priorities[index_of_max] = current_depth - i; + + if (round_robin_stage > 0) [[unlikely]] { + for (size_t attempt = 0; attempt < threads.size(); ++attempt) { + auto i = (++last_chosen) % threads.size(); + int task_index = this->GetNextTaskInThread(i); + if (task_index == threads[i].size() || + threads[i][task_index]->IsBlocked()) { + continue; + } + index_of_max = i; + max = priorities[i]; + break; } + // debug(stderr, "round robin choose: %d\n", index_of_max); + if (round_robin_start == index_of_max) { + --round_robin_stage; + } + } + + // TODO: Choose wiser constant + if (count_chosen_same == 1000 && index_of_max == last_chosen) [[unlikely]] { + round_robin_stage = 5; + round_robin_start = index_of_max; + } + + if (index_of_max == last_chosen) { + ++count_chosen_same; + } else { + count_chosen_same = 1; } + if (max == std::numeric_limits::min()) { + return std::nullopt; + } + + last_chosen = index_of_max; // Picked thread is `index_of_max` int next_task_index = this->GetNextTaskInThread(index_of_max); bool is_new = round_schedule[index_of_max] != next_task_index; @@ -160,7 +172,7 @@ struct PctStrategy : public BaseStrategyWithThreads { } thread = StableVector(); } - //this->state.Reset(); + // this->state.Reset(); UpdateStatistics(); } @@ -181,6 +193,8 @@ struct PctStrategy : public BaseStrategyWithThreads { } k_statistics.push_back(current_schedule_length); current_schedule_length = 0; + count_chosen_same = 0; + round_robin_stage = 0; // current_depth have been increased size_t new_k = std::reduce(k_statistics.begin(), k_statistics.end()) / @@ -189,25 +203,9 @@ struct PctStrategy : public BaseStrategyWithThreads { PrepareForDepth(current_depth, new_k); } - std::unordered_set CountNames(size_t except_thread) { - std::unordered_set names; - - for (size_t i = 0; i < this->threads.size(); ++i) { - auto& thread = this->threads[i]; - if (thread.empty() || i == except_thread) { - continue; - } - - auto& task = thread.back(); - names.insert(std::string{task->GetName()}); - } - - return names; - } - void PrepareForDepth(size_t depth, size_t k) { // Generates priorities - priorities = std::vector(threads_count); + priorities = std::vector(this->threads_count); for (size_t i = 0; i < priorities.size(); ++i) { priorities[i] = current_depth + i; } @@ -223,15 +221,15 @@ struct PctStrategy : public BaseStrategyWithThreads { } std::vector k_statistics; - size_t threads_count; size_t current_depth; size_t current_schedule_length; - std::vector priorities; + // NOTE(kmitkin): added for livelock avoiding in spinlocks (read more in + // original article) + size_t count_chosen_same; + size_t last_chosen; + size_t round_robin_start; + size_t round_robin_stage{0}; + std::vector priorities; std::vector priority_change_points; - // Strategy struct is the owner of all tasks, and all - // references can't be invalidated before the end of the round, - // so we have to contains all tasks in queues(queue doesn't invalidate the - // references) - bool forbid_all_same; std::mt19937 rng; }; diff --git a/runtime/include/pick_strategy.h b/runtime/include/pick_strategy.h index 71530642..8a21045a 100644 --- a/runtime/include/pick_strategy.h +++ b/runtime/include/pick_strategy.h @@ -1,73 +1,31 @@ #pragma once #include +#include #include #include "scheduler.h" -template +template struct PickStrategy : public BaseStrategyWithThreads { - virtual size_t Pick() = 0; + virtual std::optional Pick() = 0; - virtual size_t PickSchedule() = 0; + virtual std::optional PickSchedule() = 0; explicit PickStrategy(size_t threads_count, std::vector constructors) - : next_task(0), threads_count(threads_count) { - this->constructors = std::move(constructors); - this->round_schedule.resize(threads_count, -1); + : BaseStrategyWithThreads(threads_count, + constructors) {} - std::random_device dev; - rng = std::mt19937(dev()); - this->constructors_distribution = - std::uniform_int_distribution( - 0, this->constructors.size() - 1); + std::optional NextThreadId() override { return Pick(); } - // Create queues. - for (size_t i = 0; i < threads_count; ++i) { - this->threads.emplace_back(); - } - } - - // If there aren't any non returned tasks and the amount of finished tasks - // is equal to the max_tasks the finished task will be returned - TaskWithMetaData Next() override { - auto& threads = this->threads; - auto current_thread = Pick(); - debug(stderr, "Picked thread: %zu\n", current_thread); - - // it's the first task if the queue is empty - if (threads[current_thread].empty() || - threads[current_thread].back()->IsReturned()) { - // a task has finished or the queue is empty, so we add a new task - std::shuffle(this->constructors.begin(), this->constructors.end(), rng); - size_t verified_constructor = -1; - for (size_t i = 0; i < this->constructors.size(); ++i) { - TaskBuilder constructor = this->constructors.at(i); - CreatedTaskMetaData next_task = {constructor.GetName(), true, - current_thread}; - if (this->sched_checker.Verify(next_task)) { - verified_constructor = i; - break; - } - } - if (verified_constructor == -1) { - assert(false && "Oops, possible deadlock or incorrect verifier\n"); - } - threads[current_thread].emplace_back( - this->constructors[verified_constructor].Build( - &this->state, current_thread, this->new_task_id++)); - TaskWithMetaData task{threads[current_thread].back(), true, - current_thread}; - return task; - } - - return {threads[current_thread].back(), false, current_thread}; - } - - TaskWithMetaData NextSchedule() override { + std::optional NextSchedule() override { auto& round_schedule = this->round_schedule; - size_t current_thread = PickSchedule(); + auto current_thread_opt = PickSchedule(); + if (!current_thread_opt.has_value()) { + return std::nullopt; + } + size_t current_thread = current_thread_opt.value(); int next_task_index = this->GetNextTaskInThread(current_thread); bool is_new = round_schedule[current_thread] != next_task_index; @@ -86,15 +44,10 @@ struct PickStrategy : public BaseStrategyWithThreads { thread.pop_back(); } } - - // Reinitial target as we start from the beginning. - //this->state.Reset(); } ~PickStrategy() { this->TerminateTasks(); } protected: size_t next_task = 0; - size_t threads_count; - std::mt19937 rng; }; diff --git a/runtime/include/pretty_print.h b/runtime/include/pretty_print.h index 145dc974..9504fa59 100644 --- a/runtime/include/pretty_print.h +++ b/runtime/include/pretty_print.h @@ -40,7 +40,7 @@ struct PrettyPrinter { return get<1>(v).thread_id; }; - int cell_width = 20; // Up it if necessary. Enough for now. + int cell_width = 50; // Up it if necessary. Enough for now. auto print_separator = [&out, this, cell_width]() { out << "*"; diff --git a/runtime/include/random_strategy.h b/runtime/include/random_strategy.h index 3765d785..9f144f7f 100644 --- a/runtime/include/random_strategy.h +++ b/runtime/include/random_strategy.h @@ -8,7 +8,7 @@ // Allows a random thread to work. // Randoms new task. -template +template struct RandomStrategy : PickStrategy { explicit RandomStrategy(size_t threads_count, std::vector constructors, @@ -17,25 +17,25 @@ struct RandomStrategy : PickStrategy { std::move(constructors)}, weights{std::move(weights)} {} - size_t Pick() override { + std::optional Pick() override { pick_weights.clear(); auto &threads = PickStrategy::threads; for (size_t i = 0; i < threads.size(); ++i) { - if (!threads[i].empty() && - (threads[i].back()->IsParked() || threads[i].back()->IsBlocked())) { + if (!threads[i].empty() && threads[i].back()->IsBlocked()) { continue; } pick_weights.push_back(weights[i]); } - assert(!pick_weights.empty() && "deadlock"); + if (pick_weights.empty()) [[unlikely]] { + return std::nullopt; + } auto thread_distribution = std::discrete_distribution<>(pick_weights.begin(), pick_weights.end()); auto num = thread_distribution(PickStrategy::rng); for (size_t i = 0; i < threads.size(); ++i) { - if (!threads[i].empty() && - (threads[i].back()->IsParked() || threads[i].back()->IsBlocked())) { + if (!threads[i].empty() && threads[i].back()->IsBlocked()) { continue; } if (num == 0) { @@ -43,23 +43,25 @@ struct RandomStrategy : PickStrategy { } num--; } - assert(false && "Cannot pick thread to continue round generation"); + return std::nullopt; } - size_t PickSchedule() override { + std::optional PickSchedule() override { pick_weights.clear(); auto &threads = this->threads; for (size_t i = 0; i < threads.size(); ++i) { int task_index = this->GetNextTaskInThread(i); if (task_index == threads[i].size() || - threads[i][task_index]->IsParked()) { + threads[i][task_index]->IsBlocked()) { continue; } pick_weights.push_back(weights[i]); } - assert(!pick_weights.empty() && "deadlock"); + if (pick_weights.empty()) [[unlikely]] { + return std::nullopt; + } auto thread_distribution = std::discrete_distribution<>(pick_weights.begin(), pick_weights.end()); @@ -67,7 +69,7 @@ struct RandomStrategy : PickStrategy { for (size_t i = 0; i < threads.size(); ++i) { int task_index = this->GetNextTaskInThread(i); if (task_index == threads[i].size() || - threads[i][task_index]->IsParked()) { + threads[i][task_index]->IsBlocked()) { continue; } if (num == 0) { @@ -75,7 +77,7 @@ struct RandomStrategy : PickStrategy { } num--; } - assert(false && "Cannot pick thread to continue round scheduling"); + return std::nullopt; } private: diff --git a/runtime/include/round_robin_strategy.h b/runtime/include/round_robin_strategy.h index b8ae99a1..7045308e 100644 --- a/runtime/include/round_robin_strategy.h +++ b/runtime/include/round_robin_strategy.h @@ -4,7 +4,7 @@ #include "pick_strategy.h" -template +template struct RoundRobinStrategy : PickStrategy { explicit RoundRobinStrategy(size_t threads_count, std::vector constructors) @@ -12,32 +12,31 @@ struct RoundRobinStrategy : PickStrategy { PickStrategy{threads_count, std::move(constructors)} {} - size_t Pick() override { + std::optional Pick() override { auto &threads = PickStrategy::threads; for (size_t attempt = 0; attempt < threads.size(); ++attempt) { auto cur = (next_task++) % threads.size(); - if (!threads[cur].empty() && (threads[cur].back()->IsParked() || - threads[cur].back()->IsBlocked())) { + if (!threads[cur].empty() && threads[cur].back()->IsBlocked()) { continue; } return cur; } - assert(false && "deadlock"); + return std::nullopt; } - size_t PickSchedule() override { + std::optional PickSchedule() override { auto &threads = this->threads; for (size_t attempt = 0; attempt < threads.size(); ++attempt) { auto cur = (next_task++) % threads.size(); int task_index = this->GetNextTaskInThread(cur); if (task_index == threads[cur].size() || - threads[cur][task_index]->IsParked()) { + threads[cur][task_index]->IsBlocked()) { continue; } return cur; } - assert(false && "deadlock"); + return std::nullopt; } size_t next_task; diff --git a/runtime/include/scheduler.h b/runtime/include/scheduler.h index a6c22e2e..81f47ebf 100644 --- a/runtime/include/scheduler.h +++ b/runtime/include/scheduler.h @@ -18,48 +18,36 @@ #include "scheduler_fwd.h" #include "stable_vector.h" -/// Generated by some strategy task, -/// that may be not executed due to constraints of data structure -struct CreatedTaskMetaData { - std::string name; - bool is_new; - size_t thread_id; -}; - struct TaskWithMetaData { Task& task; bool is_new; size_t thread_id; }; -/// StrategyVerifier is required for scheduling only allowed tasks +/// StrategyTaskVerifier is required for scheduling only allowed tasks /// Some data structures doesn't allow us to schedule one tasks before another /// e.g. Mutex -- we are not allowed to schedule unlock before lock call, it is /// UB. template -concept StrategyVerifier = requires(T a) { - { - a.Verify(CreatedTaskMetaData(string(), bool(), int())) - } -> std::same_as; - { - a.OnFinished(TaskWithMetaData(std::declval(), bool(), int())) - } -> std::same_as; - { a.Reset() } -> std::same_as; - { a.UpdateState(std::string_view(), int(), bool()) } -> std::same_as; +concept StrategyTaskVerifier = requires(T a) { + { a.Verify(std::declval(), size_t()) } -> std::same_as; + { a.OnFinished(std::declval(), size_t()) } -> std::same_as; + { a.ReleaseTask(size_t()) } -> std::same_as>; }; // Strategy is the general strategy interface which decides which task // will be the next one it can be implemented by different strategies, such as: // randomized/tla/fair -// template struct Strategy { - virtual TaskWithMetaData Next() = 0; + virtual std::optional NextThreadId() = 0; + + virtual std::optional Next() = 0; // Returns the same data as `Next` method. However, it does not generate the // round by inserting new tasks in it, but schedules the threads accoding to // the strategy policy with previously genereated and saved round (used for // round replaying functionality) - virtual TaskWithMetaData NextSchedule() = 0; + virtual std::optional NextSchedule() = 0; // Returns { task, its thread id } (TODO: make it `const` method) virtual std::optional> GetTask(int task_id) = 0; @@ -100,7 +88,7 @@ struct Strategy { // Called when the finished task must be reported to the verifier // (Strategy is a pure interface, the templated subclass // BaseStrategyWithThreads knows about the Verifier and will delegate to that) - virtual void OnVerifierTaskFinish(TaskWithMetaData task) = 0; + virtual void OnVerifierTaskFinish(Task& task, size_t thread_id) = 0; virtual ~Strategy() = default; @@ -118,8 +106,28 @@ struct Strategy { std::vector round_schedule; }; -template +template struct BaseStrategyWithThreads : public Strategy { + BaseStrategyWithThreads(size_t threads_count, + std::vector constructors) + : state(std::make_unique()), + threads_count(threads_count), + constructors(std::move(constructors)) { + round_schedule.resize(threads_count, -1); + + constructors_distribution = + std::uniform_int_distribution( + 0, constructors.size() - 1); + + // Create queues. + for (size_t i = 0; i < threads_count; ++i) { + threads.emplace_back(); + } + + std::random_device dev; + rng = std::mt19937(dev()); + } + std::optional> GetTask(int task_id) override { // TODO: can this be optimized? int thread_id = 0; @@ -145,12 +153,11 @@ struct BaseStrategyWithThreads : public Strategy { void ResetCurrentRound() override { TerminateTasks(); - // state.Reset(); for (auto& thread : threads) { size_t tasks_in_thread = thread.size(); for (size_t i = 0; i < tasks_in_thread; ++i) { if (!IsTaskRemoved(thread[i]->GetId())) { - thread[i] = thread[i]->Restart(&state); + thread[i] = thread[i]->Restart(state.get()); } } } @@ -179,16 +186,49 @@ struct BaseStrategyWithThreads : public Strategy { int GetThreadsCount() const override { return threads.size(); } - void OnVerifierTaskFinish(TaskWithMetaData task) override { - sched_checker.OnFinished(task); + void OnVerifierTaskFinish(Task& task, size_t thread_id) override { + sched_checker.OnFinished(task, thread_id); + } + + std::optional Next() override { + return NextVerifiedFor(NextThreadId()); + } + + std::optional NextVerifiedFor( + std::optional opt_thread_index) { + if (!opt_thread_index.has_value()) { + return std::nullopt; + } + size_t thread_index = opt_thread_index.value(); + // it's the first task if the queue is empty + bool is_new = threads[thread_index].empty() || + threads[thread_index].back()->IsReturned(); + if (is_new) { + // a task has finished or the queue is empty, so we add a new task + std::shuffle(this->constructors.begin(), this->constructors.end(), rng); + size_t verified_constructor = -1; + for (size_t i = 0; i < this->constructors.size(); ++i) { + TaskBuilder constructor = this->constructors.at(i); + if (this->sched_checker.Verify(constructor.GetName(), thread_index)) { + verified_constructor = i; + break; + } + } + if (verified_constructor == -1) { + return std::nullopt; + } + threads[thread_index].emplace_back( + this->constructors[verified_constructor].Build( + this->state.get(), thread_index, this->new_task_id++)); + } + + return TaskWithMetaData{threads[thread_index].back(), is_new, thread_index}; } protected: // Terminates all running tasks. // We do it in a dangerous way: in random order. // Actually, we assume obstruction free here. - // TODO: for locks we need to figure out how to properly terminate: see - // https://github.com/ITMO-PTDC-Team/LTest/issues/13 void TerminateTasks() { auto& round_schedule = this->round_schedule; assert(round_schedule.size() == this->threads.size() && @@ -197,7 +237,6 @@ struct BaseStrategyWithThreads : public Strategy { std::vector task_indexes(this->threads.size(), 0); bool has_nonterminated_threads = true; - while (has_nonterminated_threads) { has_nonterminated_threads = false; @@ -211,31 +250,40 @@ struct BaseStrategyWithThreads : public Strategy { task_index++; } - if (task_index < thread.size()) { - auto& task = thread[task_index]; + if (task_index == thread.size()) { + std::optional releaseTask = + this->sched_checker.ReleaseTask(thread_index); + // Check if we should schedule release task to unblock other tasks + if (releaseTask) { + auto constructor = + *std::find_if(constructors.begin(), constructors.end(), + [=](const TaskBuilder& b) { + return b.GetName() == *releaseTask; + }); + auto task = + constructor.Build(this->state.get(), thread_index, task_index); + auto verified = this->sched_checker.Verify( + std::string(task->GetName()), thread_index); + thread.emplace_back(task); + } + } - // if task is blocked and it is the last one, then just increment the - // task index - if (task->IsBlocked()) { - assert(task_index == thread.size() - 1 && - "Trying to terminate blocked task, which is not last in the " - "thread."); - if (task_index == thread.size() - 1) { - task_index++; - } - } else { - has_nonterminated_threads = true; - // do a single step in this task - task->Resume(); + if (task_index < thread.size() && !thread[task_index]->IsBlocked()) { + auto& task = thread[task_index]; + has_nonterminated_threads = true; + // do a single step in this task + task->Resume(); + if (task->IsReturned()) { + OnVerifierTaskFinish(task, thread_index); + debug(stderr, "Terminated: %ld\n", thread_index); } } } } - - this->sched_checker.Reset(); - state.Reset(); + state.reset(new TargetObj{}); } + /// Returns task id in threads[thread_index] skipping removed tasks int GetNextTaskInThread(int thread_index) const override { auto& thread = threads[thread_index]; int task_index = round_schedule[thread_index]; @@ -250,20 +298,22 @@ struct BaseStrategyWithThreads : public Strategy { } Verifier sched_checker{}; - TargetObj state{}; + std::unique_ptr state; // Strategy struct is the owner of all tasks, and all // references can't be invalidated before the end of the round, // so we have to contains all tasks in queues(queue doesn't invalidate the // references) + size_t threads_count; std::vector> threads; std::vector constructors; std::uniform_int_distribution constructors_distribution; + std::mt19937 rng; }; // StrategyScheduler generates different sequential histories (using Strategy) // and then checks them with the ModelChecker -template +template struct StrategyScheduler : public SchedulerWithReplay { // max_switches represents the maximal count of switches. After this count // scheduler will end execution of the Run function @@ -286,11 +336,10 @@ struct StrategyScheduler : public SchedulerWithReplay { Scheduler::Result Run() override { for (size_t i = 0; i < max_rounds; ++i) { log() << "run round: " << i << "\n"; - debug(stderr, "run round: %d\n", i); auto histories = RunRound(); if (histories.has_value()) { - auto& [full_history, sequential_history] = histories.value(); + auto& [full_history, sequential_history, reason] = histories.value(); if (should_minimize_history) { log() << "Full nonlinear scenario: \n"; @@ -334,11 +383,15 @@ struct StrategyScheduler : public SchedulerWithReplay { // Full history of the current execution in the Run function FullHistory full_history; - for (size_t finished_tasks = 0; finished_tasks < max_tasks;) { - debug(stderr, "Tasks finished: %d\n", finished_tasks); + bool deadlock_detected{false}; + for (size_t finished_tasks = 0; finished_tasks < max_tasks;) { auto t = strategy.Next(); - auto& [next_task, is_new, thread_id] = t; + if (!t.has_value()) { + deadlock_detected = true; + break; + } + auto [next_task, is_new, thread_id] = t.value(); // fill the sequential history if (is_new) { @@ -349,17 +402,25 @@ struct StrategyScheduler : public SchedulerWithReplay { next_task->Resume(); if (next_task->IsReturned()) { finished_tasks++; - strategy.OnVerifierTaskFinish(t); + strategy.OnVerifierTaskFinish(next_task, thread_id); auto result = next_task->GetRetVal(); sequential_history.emplace_back(Response(next_task, result, thread_id)); + debug(stderr, "Tasks finished: %ld\n", finished_tasks); } } pretty_printer.PrettyPrint(sequential_history, log()); + if (deadlock_detected) { + return NonLinearizableHistory(full_history, sequential_history, + NonLinearizableHistory::Reason::DEADLOCK); + } + if (!checker.Check(sequential_history)) { - return std::make_pair(full_history, sequential_history); + return NonLinearizableHistory( + full_history, sequential_history, + NonLinearizableHistory::Reason::NON_LINEARIZABLE_HISTORY); } return std::nullopt; @@ -373,9 +434,16 @@ struct StrategyScheduler : public SchedulerWithReplay { SeqHistory sequential_history; FullHistory full_history; + bool deadlock_detected{false}; + for (int tasks_to_run = strategy.GetValidTasksCount(); tasks_to_run > 0;) { - auto [next_task, is_new, thread_id] = strategy.NextSchedule(); + auto t = strategy.NextSchedule(); + if (!t.has_value()) { + deadlock_detected = true; + break; + } + auto [next_task, is_new, thread_id] = t.value(); if (is_new) { sequential_history.emplace_back(Invoke(next_task, thread_id)); @@ -385,6 +453,7 @@ struct StrategyScheduler : public SchedulerWithReplay { next_task->Resume(); if (next_task->IsReturned()) { tasks_to_run--; + strategy.OnVerifierTaskFinish(next_task, thread_id); auto result = next_task->GetRetVal(); sequential_history.emplace_back( @@ -392,10 +461,17 @@ struct StrategyScheduler : public SchedulerWithReplay { } } + if (deadlock_detected) { + return NonLinearizableHistory(full_history, sequential_history, + NonLinearizableHistory::Reason::DEADLOCK); + } + if (!checker.Check(sequential_history)) { // log() << "New nonlinearized scenario:\n"; // pretty_printer.PrettyPrint(sequential_history, log()); - return std::make_pair(full_history, sequential_history); + return NonLinearizableHistory( + full_history, sequential_history, + NonLinearizableHistory::Reason::NON_LINEARIZABLE_HISTORY); } } @@ -456,7 +532,9 @@ struct StrategyScheduler : public SchedulerWithReplay { // pretty_printer.PrettyPrint(sequential_history, log()); if (!checker.Check(sequential_history)) { - return std::make_pair(full_history, sequential_history); + return NonLinearizableHistory( + full_history, sequential_history, + NonLinearizableHistory::Reason::NON_LINEARIZABLE_HISTORY); } return std::nullopt; @@ -466,7 +544,7 @@ struct StrategyScheduler : public SchedulerWithReplay { // Minimizes number of tasks in the nonlinearized history preserving threads // interleaving. Modifies argument `nonlinear_history`. - void Minimize(BothHistories& nonlinear_history, + void Minimize(NonLinearizableHistory& nonlinear_history, const RoundMinimizor& minimizor) override { minimizor.Minimize(*this, nonlinear_history); } @@ -483,7 +561,7 @@ struct StrategyScheduler : public SchedulerWithReplay { }; // TLAScheduler generates all executions satisfying some conditions. -template +template struct TLAScheduler : Scheduler { TLAScheduler(size_t max_tasks, size_t max_rounds, size_t threads_count, size_t max_switches, size_t max_depth, @@ -503,6 +581,7 @@ struct TLAScheduler : Scheduler { .tasks = StableVector{}, }); } + state = std::make_unique(); }; Scheduler::Result Run() override { @@ -558,7 +637,7 @@ struct TLAScheduler : Scheduler { // Firstly, terminate all running tasks. TerminateTasks(); // In histories we store references, so there's no need to update it. - state.Reset(); + state.reset(new TargetObj{}); for (size_t step = 0; step < step_end; ++step) { auto& frame = frames[step]; auto task = frame.task; @@ -566,7 +645,7 @@ struct TLAScheduler : Scheduler { if (frame.is_new) { // It was a new task. // So restart it from the beginning with the same args. - *task = (*task)->Restart(&state); + *task = (*task)->Restart(state.get()); } else { // It was a not new task, hence, we recreated in early. } @@ -581,20 +660,18 @@ struct TLAScheduler : Scheduler { assert(coroutine_status->has_started); full_history.emplace_back(thread_id, task); } - //To prevent cases like this - // +--------+--------+ - // | T1 | T2 | - // +--------+--------+ - // | | Recv | - // | Send | | - // | | >read | - // | >flush | | - // +--------+--------+ - verifier.UpdateState(coroutine_status->name, thread_id, coroutine_status->has_started); + // To prevent cases like this + // +--------+--------+ + // | T1 | T2 | + // +--------+--------+ + // | | Recv | + // | Send | | + // | | >read | + // | >flush | | + // +--------+--------+ full_history.emplace_back(thread_id, coroutine_status.value()); coroutine_status.reset(); } else { - verifier.UpdateState(task->GetName(), thread_id, is_new); full_history.emplace_back(thread_id, task); } } @@ -625,13 +702,13 @@ struct TLAScheduler : Scheduler { sequential_history.emplace_back(Invoke(task, thread_id)); } - assert(!task->IsParked()); + assert(!task->IsBlocked()); task->Resume(); UpdateFullHistory(thread_id, task, is_new); bool is_finished = task->IsReturned(); if (is_finished) { finished_tasks++; - verifier.OnFinished(TaskWithMetaData{task, false, thread.id}); + verifier.OnFinished(task, thread.id); auto result = task->GetRetVal(); sequential_history.emplace_back(Response(task, result, thread_id)); } @@ -652,7 +729,9 @@ struct TLAScheduler : Scheduler { ++finished_rounds; if (!checker.Check(sequential_history)) { return {false, - std::make_pair(Scheduler::FullHistory{}, sequential_history)}; + NonLinearizableHistory( + FullHistory{}, sequential_history, + NonLinearizableHistory::Reason::NON_LINEARIZABLE_HISTORY)}; } if (finished_rounds == max_rounds) { // It was the last round. @@ -703,12 +782,11 @@ struct TLAScheduler : Scheduler { auto& thread = threads[i]; auto& tasks = thread.tasks; if (!tasks.empty() && !tasks.back()->IsReturned()) { - if (tasks.back()->IsParked()) { + if (tasks.back()->IsBlocked()) { continue; } all_parked = false; - if (!verifier.Verify(CreatedTaskMetaData{ - std::string{tasks.back()->GetName()}, false, i})) { + if (!verifier.Verify(std::string{tasks.back()->GetName()}, i)) { continue; } // Task exists. @@ -728,7 +806,7 @@ struct TLAScheduler : Scheduler { bool stop = started_tasks == max_tasks; if (!stop && threads[i].tasks.size() < max_depth) { for (auto cons : constructors) { - if (!verifier.Verify(CreatedTaskMetaData{cons.GetName(), true, i})) { + if (!verifier.Verify(cons.GetName(), i)) { continue; } frame.is_new = true; @@ -767,12 +845,12 @@ struct TLAScheduler : Scheduler { size_t started_tasks{}; size_t finished_tasks{}; size_t finished_rounds{}; - TargetObj state{}; + std::unique_ptr state; std::vector> sequential_history; FullHistoryWithThreads full_history; std::vector thread_id_history; StableVector threads; StableVector frames; - Verifier verifier; + Verifier verifier{}; std::function cancel; }; diff --git a/runtime/include/scheduler_fwd.h b/runtime/include/scheduler_fwd.h index 1ab0e612..5e0900f1 100644 --- a/runtime/include/scheduler_fwd.h +++ b/runtime/include/scheduler_fwd.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include #include @@ -12,8 +11,15 @@ struct RoundMinimizor; struct Scheduler { using FullHistory = std::vector>; using SeqHistory = std::vector>; - using BothHistories = std::pair; - using Result = std::optional; + + struct NonLinearizableHistory { + enum class Reason { DEADLOCK, NON_LINEARIZABLE_HISTORY }; + FullHistory full; + SeqHistory seq; + Reason reason; + }; + + using Result = std::optional; virtual Result Run() = 0; @@ -35,6 +41,6 @@ struct SchedulerWithReplay : Scheduler { virtual Strategy& GetStrategy() const = 0; - virtual void Minimize(BothHistories& nonlinear_history, + virtual void Minimize(NonLinearizableHistory& nonlinear_history, const RoundMinimizor& minimizor) = 0; }; \ No newline at end of file diff --git a/runtime/include/strategy_verifier.h b/runtime/include/strategy_verifier.h index edba95ea..75edf5ae 100644 --- a/runtime/include/strategy_verifier.h +++ b/runtime/include/strategy_verifier.h @@ -1,11 +1,12 @@ #pragma once #include "scheduler.h" -struct DefaultStrategyVerifier { - inline bool Verify(CreatedTaskMetaData task) { return true; } +struct DefaultStrategyTaskVerifier { + inline bool Verify(const string& name, size_t thread_id) { return true; } - inline void OnFinished(TaskWithMetaData task) {} + inline void OnFinished(Task& task, size_t thread_id) {} - inline void Reset() {} - inline void UpdateState(std::string_view, int, bool){} + inline std::optional ReleaseTask(size_t thread_id) { + return std::nullopt; + } }; diff --git a/runtime/include/syscall_trap.h b/runtime/include/syscall_trap.h index 196e5f18..33f525dd 100644 --- a/runtime/include/syscall_trap.h +++ b/runtime/include/syscall_trap.h @@ -1,6 +1,6 @@ #pragma once -extern bool __trap_syscall; +extern bool ltest_trap_syscall; namespace ltest { diff --git a/runtime/include/value_wrapper.h b/runtime/include/value_wrapper.h index 8bda0791..434ff0f4 100644 --- a/runtime/include/value_wrapper.h +++ b/runtime/include/value_wrapper.h @@ -28,8 +28,8 @@ class ValueWrapper { bool operator==(const ValueWrapper& other) const { return compare(*this, other); } - //using std::to_string - friend std::string to_string(const ValueWrapper& wrapper) { // NOLINT + // using std::to_string + friend std::string to_string(const ValueWrapper& wrapper) { // NOLINT return wrapper.to_str(wrapper); } bool HasValue() const { return value.has_value(); } diff --git a/runtime/include/verifying.h b/runtime/include/verifying.h index c4391dc4..848b9dfd 100644 --- a/runtime/include/verifying.h +++ b/runtime/include/verifying.h @@ -4,6 +4,7 @@ #include #include +#include "blocking_primitives.h" #include "lib.h" #include "lincheck_recursive.h" #include "logger.h" @@ -75,7 +76,7 @@ Opts ParseOpts(); std::vector split(const std::string &s, char delim); -template +template std::unique_ptr MakeStrategy(Opts &opts, std::vector l) { switch (opts.typ) { case RR: { @@ -98,8 +99,8 @@ std::unique_ptr MakeStrategy(Opts &opts, std::vector l) { } case PCT: { std::cout << "pct\n"; - return std::make_unique>( - opts.threads, std::move(l), opts.forbid_all_same); + return std::make_unique>(opts.threads, + std::move(l)); } default: assert(false && "unexpected type"); @@ -108,7 +109,7 @@ std::unique_ptr MakeStrategy(Opts &opts, std::vector l) { // Keeps pointer to strategy to pass reference to base scheduler. // TODO: refactor. -template +template struct StrategySchedulerWrapper : StrategyScheduler { StrategySchedulerWrapper(std::unique_ptr strategy, ModelChecker &checker, PrettyPrinter &pretty_printer, @@ -123,7 +124,7 @@ struct StrategySchedulerWrapper : StrategyScheduler { std::unique_ptr strategy; }; -template +template std::unique_ptr MakeScheduler(ModelChecker &checker, Opts &opts, const std::vector &l, PrettyPrinter &pretty_printer, @@ -157,16 +158,26 @@ inline int TrapRun(std::unique_ptr &&scheduler, auto guard = SyscallTrapGuard{}; auto result = scheduler->Run(); if (result.has_value()) { - std::cout << "non linearized:\n"; - pretty_printer.PrettyPrint(result.value().second, std::cout); - return 1; + if (result->reason == Scheduler::NonLinearizableHistory::Reason::DEADLOCK) { + std::cout << "deadlock detected:\n"; + pretty_printer.PrettyPrint(result->seq, std::cout); + return 4; // see https://tldp.org/LDP/abs/html/exitcodes.html + } else if (result->reason == Scheduler::NonLinearizableHistory::Reason:: + NON_LINEARIZABLE_HISTORY) { + std::cout << "non linearized:\n"; + pretty_printer.PrettyPrint(result->seq, std::cout); + return 3; + } else { + std::abort(); + } } else { std::cout << "success!\n"; return 0; } } -template +template int Run(int argc, char *argv[]) { if constexpr (!std::is_same_v) { diff --git a/runtime/include/verifying_macro.h b/runtime/include/verifying_macro.h index 50b66401..466f2d7b 100644 --- a/runtime/include/verifying_macro.h +++ b/runtime/include/verifying_macro.h @@ -18,6 +18,12 @@ extern std::vector task_builders; // Tell that the function need to be converted to the coroutine. #define non_atomic attr(ltest_nonatomic) +// Tell that the function must not contain interleavings. +// Note that all functions that can be reached recursively from this function is +// marked `as_atomic` also, so in the implementation of these functions it is +// highly recommend to avoid std:: usage otherwise many interleavings in +// standard library will not be inserted. +#define as_atomic attr(ltest_atomic) namespace ltest { @@ -25,11 +31,12 @@ template std::string toString(const T &a); template -std::string toString(const T &a) requires (std::is_integral_v){ +std::string toString(const T &a) + requires(std::is_integral_v) +{ return std::to_string(a); } - template auto toStringListHelper(const tuple_t &t, std::index_sequence) noexcept { @@ -53,7 +60,7 @@ auto toStringArgs(std::shared_ptr args) { } template -struct TargetMethod{ +struct TargetMethod { using Method = std::function; TargetMethod(std::string_view method_name, std::function(size_t)> gen, Method method) { @@ -64,10 +71,6 @@ struct TargetMethod{ auto coro = Coro::New(method, this_ptr, args, <est::toStringArgs, method_name, task_id); - if (ltest::generators::generated_token) { - coro->SetToken(ltest::generators::generated_token); - ltest::generators::generated_token.reset(); - } return coro; }; ltest::task_builders.push_back( @@ -75,7 +78,6 @@ struct TargetMethod{ } }; - template struct TargetMethod { using Method = std::function; @@ -86,16 +88,13 @@ struct TargetMethod { method = std::move(method)]( void *this_ptr, size_t thread_num, int task_id) -> Task { auto wrapper = [f = std::move(method)](void *this_ptr, Args &&...args) { - f(reinterpret_cast(this_ptr), std::forward(args)...); - return void_v; - }; auto args = std::shared_ptr(new std::tuple(gen(thread_num))); + f(reinterpret_cast(this_ptr), std::forward(args)...); + return void_v; + }; + auto args = std::shared_ptr(new std::tuple(gen(thread_num))); auto coro = Coro::New(wrapper, this_ptr, args, <est::toStringArgs, method_name, task_id); - if (ltest::generators::generated_token) { - coro->SetToken(ltest::generators::generated_token); - ltest::generators::generated_token.reset(); - } return coro; }; ltest::task_builders.push_back( @@ -105,7 +104,8 @@ struct TargetMethod { } // namespace ltest -#define declare_task_name(symbol) const char *symbol##_task_name = #symbol +#define declare_task_name(symbol) \ + static const char *symbol##_task_name = #symbol #define target_method(gen, ret, cls, symbol, ...) \ declare_task_name(symbol); \ diff --git a/runtime/include/yield_guard.h b/runtime/include/yield_guard.h new file mode 100644 index 00000000..1960c0bc --- /dev/null +++ b/runtime/include/yield_guard.h @@ -0,0 +1,12 @@ +#pragma once + +extern bool ltest_yield; + +namespace ltest { + +struct AllowYieldArea { + AllowYieldArea(); + ~AllowYieldArea(); +}; + +} // namespace ltest \ No newline at end of file diff --git a/runtime/lib.cpp b/runtime/lib.cpp index 2292d76f..3c6ce74c 100644 --- a/runtime/lib.cpp +++ b/runtime/lib.cpp @@ -1,11 +1,12 @@ #include "include/lib.h" #include -#include #include #include +#include "logger.h" #include "value_wrapper.h" +#include "yield_guard.h" // See comments in the lib.h. Task this_coro{}; @@ -13,7 +14,7 @@ Task this_coro{}; boost::context::fiber_context sched_ctx; std::optional coroutine_status; -std::unordered_map futex_state{}; +BlockManager block_manager; namespace ltest { std::vector task_builders{}; @@ -21,16 +22,23 @@ std::vector task_builders{}; Task CoroBase::GetPtr() { return shared_from_this(); } -void CoroBase::SetToken(std::shared_ptr token) { this->token = token; } - void CoroBase::Resume() { this_coro = this->GetPtr(); assert(!this_coro->IsReturned() && this_coro->ctx); - boost::context::fiber_context([](boost::context::fiber_context&& ctx) { - sched_ctx = std::move(ctx); - this_coro->ctx = std::move(this_coro->ctx).resume(); - return std::move(sched_ctx); - }).resume(); + // debug(stderr, "name: %s\n", + // std::string(this_coro->GetPtr()->GetName()).c_str()); + auto coro = this_coro.get(); // std::shared_ptr also can be interleaved + // NOTE(kmitkin): Guard below prevents us from call CoroYield in the scheduler + // coroutine, area that protected by it should be as small as possible to + // reduce errors + { + ltest::AllowYieldArea guard{}; + boost::context::fiber_context([coro](boost::context::fiber_context&& ctx) { + sched_ctx = std::move(ctx); + coro->ctx = std::move(coro->ctx).resume(); + return std::move(sched_ctx); + }).resume(); + } this_coro.reset(); } @@ -41,8 +49,6 @@ ValueWrapper CoroBase::GetRetVal() const { return ret; } -bool CoroBase::IsParked() const { return token != nullptr && token->parked; } - CoroBase::~CoroBase() { // The coroutine must be returned if we want to restart it. // We can't just Terminate() it because it is the runtime responsibility to @@ -55,6 +61,9 @@ std::string_view CoroBase::GetName() const { return name; } bool CoroBase::IsReturned() const { return is_returned; } extern "C" void CoroYield() { + if (!ltest_yield) [[unlikely]] { + return; + } assert(this_coro && sched_ctx); boost::context::fiber_context([](boost::context::fiber_context&& ctx) { this_coro->ctx = std::move(ctx); @@ -73,16 +82,13 @@ void CoroBase::Terminate() { while (!IsReturned()) { ++tries; Resume(); - assert(tries < 10000000 && + assert(tries < 1000000 && "coroutine is spinning too long, possible wrong terminating order"); } } -void Token::Reset() { parked = false; } - -void Token::Park() { - parked = true; - CoroYield(); -} - -void Token::Unpark() { parked = false; } +void CoroBase::TryTerminate() { + for (size_t i = 0; i < 1000 && !is_returned; ++i) { + Resume(); + } +} \ No newline at end of file diff --git a/runtime/minimization.cpp b/runtime/minimization.cpp index 3ff04214..d678ddb3 100644 --- a/runtime/minimization.cpp +++ b/runtime/minimization.cpp @@ -19,10 +19,10 @@ std::vector RoundMinimizor::GetTasksOrdering( // greedy void GreedyRoundMinimizor::Minimize( SchedulerWithReplay& sched, - Scheduler::BothHistories& nonlinear_history) const { + Scheduler::NonLinearizableHistory& nonlinear_history) const { std::vector> tasks; - for (const HistoryEvent& event : nonlinear_history.second) { + for (const HistoryEvent& event : nonlinear_history.seq) { if (std::holds_alternative(event)) { tasks.push_back(std::get(event).GetTask()); } @@ -38,8 +38,8 @@ void GreedyRoundMinimizor::Minimize( OnTasksRemoved(sched, nonlinear_history, {task.get()->GetId()}); if (new_histories.has_value()) { - nonlinear_history.first.swap(new_histories.value().first); - nonlinear_history.second.swap(new_histories.value().second); + nonlinear_history.full.swap(new_histories.value().full); + nonlinear_history.seq.swap(new_histories.value().seq); strategy.SetTaskRemoved(task.get()->GetId(), true); } } @@ -64,8 +64,8 @@ void GreedyRoundMinimizor::Minimize( if (new_histories.has_value()) { // sequential history (Invoke/Response events) could have odd number of // history events in case if some task are not completed which is allowed by linearizability checker - nonlinear_history.first.swap(new_histories.value().first); - nonlinear_history.second.swap(new_histories.value().second); + nonlinear_history.full.swap(new_histories.value().full); + nonlinear_history.seq.swap(new_histories.value().seq); strategy.SetTaskRemoved(task_i_id, true); strategy.SetTaskRemoved(task_j_id, true); @@ -78,16 +78,16 @@ void GreedyRoundMinimizor::Minimize( // (because multiple failed attempts to minimize new scenarios could leave // tasks in invalid state) sched.ReplayRound( - RoundMinimizor::GetTasksOrdering(nonlinear_history.first, {})); + RoundMinimizor::GetTasksOrdering(nonlinear_history.full, {})); } // same interleaving Scheduler::Result SameInterleavingMinimizor::OnTasksRemoved( SchedulerWithReplay& sched, - const Scheduler::BothHistories& nonlinear_history, + const Scheduler::NonLinearizableHistory& nonlinear_history, const std::unordered_set& task_ids) const { std::vector new_ordering = - RoundMinimizor::GetTasksOrdering(nonlinear_history.first, task_ids); + RoundMinimizor::GetTasksOrdering(nonlinear_history.full, task_ids); return sched.ReplayRound(new_ordering); } @@ -97,7 +97,7 @@ StrategyExplorationMinimizor::StrategyExplorationMinimizor(int runs_) Scheduler::Result StrategyExplorationMinimizor::OnTasksRemoved( SchedulerWithReplay& sched, - const Scheduler::BothHistories& nonlinear_history, + const Scheduler::NonLinearizableHistory& nonlinear_history, const std::unordered_set& task_ids) const { auto mark_tasks_as_removed = [&](bool is_removed) { for (const auto& task_id : task_ids) { diff --git a/runtime/minimization_smart.cpp b/runtime/minimization_smart.cpp index bc047f00..e11e192f 100644 --- a/runtime/minimization_smart.cpp +++ b/runtime/minimization_smart.cpp @@ -20,7 +20,7 @@ SmartMinimizor::SmartMinimizor(int exploration_runs, int minimization_runs, void SmartMinimizor::Minimize( SchedulerWithReplay& sched, - Scheduler::BothHistories& nonlinear_histories) const { + Scheduler::NonLinearizableHistory& nonlinear_histories) const { // reset auto& strategy = sched.GetStrategy(); total_tasks = strategy.GetTotalTasksCount(); @@ -59,7 +59,7 @@ void SmartMinimizor::Minimize( // replay the round with found nonlinearized interleaving, this put the round // in correct final state and builds a `BothHistories` object auto replayed_result = sched.ReplayRound(RoundMinimizor::GetTasksOrdering( - best_solution.nonlinear_histories.first, {})); + best_solution.nonlinear_histories.full, {})); // override nonlinear history with the best solution assert(replayed_result.has_value()); @@ -194,9 +194,9 @@ std::unordered_map> SmartMinimizor::CrossProduct( } // smart minimizor solution -SmartMinimizor::Solution::Solution(const Strategy& strategy, - const Scheduler::BothHistories& histories, - int total_tasks) { +SmartMinimizor::Solution::Solution( + const Strategy& strategy, + const Scheduler::NonLinearizableHistory& histories, int total_tasks) { int total_threads = strategy.GetThreadsCount(); // copy nonlinear history diff --git a/runtime/syscall_trap.cpp b/runtime/syscall_trap.cpp index 48705b60..bde33561 100644 --- a/runtime/syscall_trap.cpp +++ b/runtime/syscall_trap.cpp @@ -2,8 +2,8 @@ /// Required for incapsulating syscall traps only in special places where it's /// really needed -bool __trap_syscall = 0; +bool ltest_trap_syscall = 0; -ltest::SyscallTrapGuard::SyscallTrapGuard() { __trap_syscall = true; } +ltest::SyscallTrapGuard::SyscallTrapGuard() { ltest_trap_syscall = true; } -ltest::SyscallTrapGuard::~SyscallTrapGuard() { __trap_syscall = false; } +ltest::SyscallTrapGuard::~SyscallTrapGuard() { ltest_trap_syscall = false; } diff --git a/runtime/verifying.cpp b/runtime/verifying.cpp index c71b9905..85f52ed2 100644 --- a/runtime/verifying.cpp +++ b/runtime/verifying.cpp @@ -3,7 +3,9 @@ #include #include +#include #include +#include namespace ltest { @@ -13,9 +15,8 @@ std::string toString(const int &a) { } template <> -std::string toString>( - const std::shared_ptr &token) { - return "token"; +std::string toString(const size_t &a) { + return std::to_string(a); } std::string toLower(std::string str) { @@ -74,11 +75,8 @@ DEFINE_int32(exploration_runs, 15, DEFINE_int32(minimization_runs, 15, "Number of minimization runs for smart minimizor"); DEFINE_int32(depth, 0, - "How many tasks can be executed on one thread(Only for TLA)"); + "How many tasks can be executed on one thread(Only for TLA)"); DEFINE_bool(verbose, false, "Verbosity"); -DEFINE_bool( - forbid_all_same, false, - "forbid scenarios that execute tasks with same name at all threads"); DEFINE_string(strategy, GetLiteral(StrategyType::RR), "Strategy"); DEFINE_string(weights, "", "comma-separated list of weights for threads"); @@ -90,7 +88,6 @@ void SetOpts(const DefaultOptions &def) { FLAGS_depth = def.depth; FLAGS_verbose = def.verbose; FLAGS_strategy = def.strategy; - FLAGS_forbid_all_same = def.forbid_all_same; FLAGS_weights = def.weights; FLAGS_exploration_runs = def.exploration_runs; FLAGS_minimization_runs = def.minimization_runs; @@ -103,8 +100,8 @@ Opts ParseOpts() { opts.tasks = FLAGS_tasks; opts.switches = FLAGS_switches; opts.rounds = FLAGS_rounds; - opts.forbid_all_same = FLAGS_forbid_all_same; - opts.minimize = FLAGS_minimize; // NOTE(dartiukhov) minimization for scenarios with locks is not supported + opts.minimize = FLAGS_minimize; // NOTE(dartiukhov) minimization for + // scenarios with locks is not supported opts.exploration_runs = FLAGS_exploration_runs; opts.minimization_runs = FLAGS_minimization_runs; opts.verbose = FLAGS_verbose; diff --git a/runtime/yield_guard.cpp b/runtime/yield_guard.cpp new file mode 100644 index 00000000..c4007ed6 --- /dev/null +++ b/runtime/yield_guard.cpp @@ -0,0 +1,9 @@ +#include "yield_guard.h" + +/// Required for incapsulating CoroYield calls only in coroutines code, allowing +/// to call methods annotated with non_atomic in scheduler fiber +bool ltest_yield = 0; + +ltest::AllowYieldArea::AllowYieldArea() { ltest_yield = true; } + +ltest::AllowYieldArea::~AllowYieldArea() { ltest_yield = false; } diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 00000000..25bac806 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +exp_code=$1 +shift 1 +LD_PRELOAD=./build/syscall_intercept/libpreload.so $@ +if [[ $? -ne $exp_code ]]; then + exit 1 +fi \ No newline at end of file diff --git a/check_ctx_speed.sh b/scripts/check_ctx_speed.sh similarity index 97% rename from check_ctx_speed.sh rename to scripts/check_ctx_speed.sh index 81dc7454..6475c1c2 100755 --- a/check_ctx_speed.sh +++ b/scripts/check_ctx_speed.sh @@ -1,4 +1,4 @@ -#!bin/bash +#!/usr/bin/env bash set -e dir=build-bench baseline=$1 diff --git a/scripts/format_code.sh b/scripts/format_code.sh index 02a455f8..9bb58a79 100755 --- a/scripts/format_code.sh +++ b/scripts/format_code.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash find runtime -iname *.h -o -iname *.cpp | xargs clang-format -style=Google -i +find clangpass -iname *.h -o -iname *.cpp | xargs clang-format -style=Google -i +find syscall_intercept -iname *.h -o -iname *.cpp | xargs clang-format -style=Google -i find test -iname *.h -o -iname *.cpp | xargs clang-format -style=Google -i find codegen -iname *.h -o -iname *.cpp | xargs clang-format -style=Google -i find verifying -iname *.h -o -iname *.cpp | xargs clang-format -style=Google -i diff --git a/syscall_intercept/CMakeLists.txt b/syscall_intercept/CMakeLists.txt index 47969f6a..d4d8ef71 100644 --- a/syscall_intercept/CMakeLists.txt +++ b/syscall_intercept/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(preload SHARED hook.cpp) target_compile_options(preload PRIVATE "-fpic") -target_link_libraries(preload PRIVATE syscall_intercept) +target_link_libraries(preload PRIVATE syscall_intercept runtime) target_include_directories(preload PRIVATE ${CMAKE_SOURCE_DIR}) # target_link_options(preload PRIVATE ${CMAKE_ASAN_FLAGS}) # target_compile_options(preload PRIVATE ${CMAKE_ASAN_FLAGS}) \ No newline at end of file diff --git a/syscall_intercept/hook.cpp b/syscall_intercept/hook.cpp index 976b7a8b..ffd64e62 100644 --- a/syscall_intercept/hook.cpp +++ b/syscall_intercept/hook.cpp @@ -2,48 +2,56 @@ #include #include #include -#include "runtime/include/logger.h" + +#include + +#include "runtime/include/block_manager.h" #include "runtime/include/lib.h" +#include "runtime/include/logger.h" #include "runtime/include/syscall_trap.h" -static int -hook(long syscall_number, - long arg0, long arg1, - long arg2, long arg3, - long arg4, long arg5, - long *result) -{ - if (!__trap_syscall) { - return 1; - } - if (syscall_number == SYS_sched_yield) { - debug(stderr, "caught sched_yield()\n"); - CoroYield(); - return 0; - } else if (syscall_number == SYS_futex) { - debug(stderr, "caught futex(0x%lx, %ld, %ld)\n", (unsigned long)arg0, arg1, arg2); - if (arg1 == FUTEX_WAIT_PRIVATE) { - this_coro->SetBlocked(arg0, arg2); - } else if (arg1 == FUTEX_WAKE_PRIVATE) { - - } else { - assert(false && "unsupported futex call"); - } - CoroYield(); - return 0; - } else { - /* - * Ignore any other syscalls - * i.e.: pass them on to the kernel - * as would normally happen. - */ - return 1; - } +static int hook(long syscall_number, long arg0, long arg1, long arg2, long arg3, + long arg4, long arg5, long *result) { + if (!ltest_trap_syscall) { + return 1; + } + if (syscall_number == SYS_sched_yield) { + debug(stderr, "caught sched_yield()\n"); + CoroYield(); + *result = 0; + return 0; + } else if (syscall_number == SYS_futex) { + debug(stderr, "caught futex(0x%lx, %ld), exp: %ld, cur: %d\n", + (unsigned long)arg0, arg1, arg2, *((int *)arg0)); + arg1 = arg1 & FUTEX_CMD_MASK; + if (arg1 == FUTEX_WAIT || arg1 == FUTEX_WAIT_BITSET) { + auto fstate = BlockState{arg0, arg2}; + if (fstate.CanBeBlocked()) { + this_coro->SetBlocked(fstate); + CoroYield(); + *result = 0; + } else { + errno = EAGAIN; + *result = 1; + } + } else if (arg1 == FUTEX_WAKE || arg1 == FUTEX_WAKE_BITSET) { + debug(stderr, "caught wake\n"); + *result = block_manager.UnblockOn(arg0, arg2); + } else { + assert(false && "unsupported futex call"); + } + return 0; + } else { + /* + * Ignore any other syscalls + * i.e.: pass them on to the kernel + * as would normally happen. + */ + return 1; + } } -static __attribute__((constructor)) void -init(void) -{ - // Set up the callback function - intercept_hook_point = hook; +static __attribute__((constructor)) void init(void) { + // Set up the callback function + intercept_hook_point = hook; } \ No newline at end of file diff --git a/test/runtime/lin_check_test.cpp b/test/runtime/lin_check_test.cpp index 3152628f..64ec4117 100644 --- a/test/runtime/lin_check_test.cpp +++ b/test/runtime/lin_check_test.cpp @@ -235,8 +235,8 @@ std::string draw_history(const std::vector& history) { Response response = std::get(event); history_string << "[" << numeration[response.GetTask()] << " res: " << response.GetTask()->GetName() - << " returned: " << to_string(response.GetTask()->GetRetVal()) - << "]\n"; + << " returned: " + << to_string(response.GetTask()->GetRetVal()) << "]\n"; } } diff --git a/test/runtime/stackfulltask_mock.h b/test/runtime/stackfulltask_mock.h index 32fd28a2..1fe6e0a7 100644 --- a/test/runtime/stackfulltask_mock.h +++ b/test/runtime/stackfulltask_mock.h @@ -17,6 +17,6 @@ class MockTask : public CoroBase { MOCK_METHOD(void*, GetArgs, (), (const, override)); MOCK_METHOD(bool, IsSuspended, (), (const)); MOCK_METHOD(void, Terminate, (), ()); - MOCK_METHOD(void, SetToken, (std::shared_ptr), ()); + MOCK_METHOD(void, TryTerminate, (), ()); virtual ~MockTask() { is_returned = true; } }; diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index c1c585ca..6cbf212b 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -3,7 +3,7 @@ include(FetchContent) FetchContent_Declare( fuzztest GIT_REPOSITORY https://github.com/google/fuzztest.git - GIT_TAG main + GIT_TAG 2025-02-14 GIT_SHALLOW TRUE SYSTEM ) diff --git a/verifying/CMakeLists.txt b/verifying/CMakeLists.txt index 4252abb7..4d0f1ed9 100644 --- a/verifying/CMakeLists.txt +++ b/verifying/CMakeLists.txt @@ -4,14 +4,49 @@ include_directories(specs) set (PASS YieldPass) set (PASS_PATH ${CMAKE_BINARY_DIR}/codegen/lib${PASS}.so) - set (COPASS CoYieldPass) set (COPASS_PATH ${CMAKE_BINARY_DIR}/codegen/lib${COPASS}.so) +set(CLANG_TOOL ClangPassTool) +set(CLANG_TOOL_EXECUTABLE ${CMAKE_BINARY_DIR}/clangpass/${CLANG_TOOL}) + find_package(Boost REQUIRED COMPONENTS context) +# prefix the actual source file name with '__tmp_' which will be generate by clang pass tool +if (APPLY_CLANG_TOOL) +set(CLANG_TOOL_TMP_PREFIX "__tmp_") +else() +set(CLANG_TOOL_TMP_PREFIX "") +endif() + function(verify_target_without_plugin target) - add_executable(${target} ${source_name}) + if (APPLY_CLANG_TOOL) + set(SOURCE_FILE_NAME ${CLANG_TOOL_TMP_PREFIX}${source_name}) + add_executable(${target} ${SOURCE_FILE_NAME}) + # apply clangpass to the ${source_name} file + add_custom_command( + OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE_NAME} + COMMAND ${CLANG_TOOL_EXECUTABLE} + -p=${CMAKE_BINARY_DIR}/compile_commands.json # passing compilation database, make sure CMAKE_EXPORT_COMPILE_COMMANDS flag is set + --temp-prefix ${CLANG_TOOL_TMP_PREFIX} + --replace-names ::std::mutex,::std::shared_mutex,::std::condition_variable + --insert-names ltest::mutex,ltest::shared_mutex,ltest::condition_variable + ${CMAKE_CURRENT_SOURCE_DIR}/${source_name} + DEPENDS ${CLANG_TOOL} + COMMENT "Running Clang Pass Tool on ${source_name}" + ) + + # delete the temp file generated by previous command + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "Removing temporary file '${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE_NAME}' generated after building ${target}" + COMMAND ${CMAKE_COMMAND} -E remove ${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE_FILE_NAME} + ) + else() + add_executable(${target} ${source_name}) + endif() + add_dependencies(${target} runtime plugin_pass) target_include_directories(${target} PRIVATE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/third_party) target_link_options(${target} PRIVATE ${CMAKE_ASAN_FLAGS}) target_compile_options(${target} PRIVATE ${CMAKE_ASAN_FLAGS}) diff --git a/verifying/blocking/CMakeLists.txt b/verifying/blocking/CMakeLists.txt index fbd660bd..aa32b7af 100644 --- a/verifying/blocking/CMakeLists.txt +++ b/verifying/blocking/CMakeLists.txt @@ -2,18 +2,26 @@ set (SOURCE_TARGET_LIST mutexed_register.cpp simple_mutex.cpp nonlinear_mutex.cpp + shared_mutexed_register.cpp + bank.cpp + buffered_channel.cpp + simple_deadlock.cpp + bank_deadlock.cpp +) + +set (FOLLY_SOURCE_TARGET_LIST + folly_sharedmutex.cpp folly_rwspinlock.cpp + folly_flatcombining_queue.cpp ) foreach(source_name ${SOURCE_TARGET_LIST}) get_filename_component(target ${source_name} NAME_WE) verify_target(${target}) add_dependencies(${target} preload) + list(APPEND VERIFY_BLOCKING_LIST ${target}) endforeach(source_name ${SOURCE_TARGET_LIST}) -#Worlaround due no folly -list(APPEND VERIFY_BLOCKING_LIST simple_mutex) - add_custom_target(verify-blocking DEPENDS ${VERIFY_BLOCKING_LIST} @@ -27,20 +35,73 @@ function (add_integration_test_blocking test_name label fail) ENVIRONMENT_MODIFICATION "LD_PRELOAD=string_append:${CMAKE_BINARY_DIR}/syscall_intercept/libpreload.so") endfunction() -# Does not work, see issue: https://github.com/ITMO-PTDC-Team/LTest/issues/12 -# add_integration_test_blocking("simple_mutex_pct" "verify" TRUE -# simple_mutex --strategy pct -# ) +add_integration_test_blocking("simple_mutex_pct" "verify" FALSE + simple_mutex --strategy pct --rounds 10000 +) add_integration_test_blocking("simple_mutex_random" "verify" FALSE - simple_mutex --strategy random + simple_mutex --strategy random --rounds 10000 +) + +add_integration_test_blocking("nonlinear_mutex_random" "verify" TRUE + nonlinear_mutex --strategy random --rounds 10000 --threads 4 +) + +add_integration_test_blocking("nonlinear_mutex_pct" "verify" TRUE + nonlinear_mutex --strategy pct --rounds 10000 +) + +add_integration_test_blocking("bank_pct" "verify" FALSE + bank --strategy pct --rounds 10000 +) + +add_integration_test_blocking("bank_random" "verify" FALSE + bank --strategy random --rounds 10000 +) + +add_integration_test_blocking("mutexed_register_pct" "verify" FALSE + mutexed_register --strategy pct --rounds 10000 +) + +add_integration_test_blocking("mutexed_register_random" "verify" FALSE + mutexed_register --strategy random --rounds 10000 +) + +add_integration_test_blocking("shared_mutexed_register_pct" "verify" FALSE + shared_mutexed_register --strategy pct --rounds 10000 ) -# Does not work, see issue: https://github.com/ITMO-PTDC-Team/LTest/issues/13 -# add_integration_test_blocking("nonlinear_mutex_random" "verify" TRUE -# nonlinear_mutex --strategy random -# ) +add_integration_test_blocking("shared_mutexed_register_random" "verify" FALSE + shared_mutexed_register --strategy random --rounds 10000 +) -# add_integration_test_blocking("nonlinear_mutex_pct" "verify" TRUE -# nonlinear_mutex --strategy pct -# ) \ No newline at end of file +add_integration_test_blocking("buffered_channel_pct" "verify" FALSE + buffered_channel --strategy pct --rounds 10000 +) + +add_integration_test_blocking("buffered_channel_random" "verify" FALSE + buffered_channel --strategy random --rounds 10000 +) + +add_integration_test_blocking("simple_deadlock_pct" "verify" TRUE + simple_deadlock --strategy pct --rounds 10000 +) + +add_integration_test_blocking("simple_deadlock_random" "verify" TRUE + simple_deadlock --strategy random --rounds 10000 +) + +add_integration_test_blocking("bank_deadlock_pct" "verify" TRUE + bank_deadlock --strategy pct --rounds 10000 +) + +add_integration_test_blocking("bank_deadlock_random" "verify" TRUE + bank_deadlock --strategy random --rounds 10000 +) + +foreach(source_name ${FOLLY_SOURCE_TARGET_LIST}) + get_filename_component(target ${source_name} NAME_WE) + verify_target(${target}) + target_link_libraries(${target} PRIVATE glog folly double-conversion) + add_dependencies(${target} preload) +endforeach(source_name ${FOLLY_SOURCE_TARGET_LIST}) diff --git a/verifying/blocking/bank.cpp b/verifying/blocking/bank.cpp new file mode 100644 index 00000000..adb6dcff --- /dev/null +++ b/verifying/blocking/bank.cpp @@ -0,0 +1,106 @@ +#include "verifying/specs/bank.h" + +#include +#include +#include +#include + +#include "runtime/include/verifying.h" + +class Bank { + private: + struct Cell { + std::shared_mutex m; + int amount; + Cell(int amount) : amount(amount) {} + }; + + std::deque cells_; + + public: + Bank() { + for (int i = 0; i < SIZE; ++i) { + cells_.emplace_back(INIT / 2); + } + } + + non_atomic void Add(int i, size_t count) { + // debug(stderr, "Add [%d] %lu\n", i, count); + std::lock_guard lock{cells_[i].m}; + cells_[i].amount += count; + } + + non_atomic int Read(int i) { + // debug(stderr, "Read [%d]\n", i); + std::shared_lock lock{cells_[i].m}; + return cells_[i].amount; + } + + non_atomic int Transfer(int i, int j, size_t count) { + // debug(stderr, "Transfer [%d] -> [%d] %lu\n", i, j, count); + + int first = std::min(i, j); + int second = std::max(i, j); + int res; + if (first == second) { + std::shared_lock lock_first{cells_[first].m}; + res = count <= cells_[i].amount; + } else { + std::lock_guard lock_first{cells_[first].m}; + { + std::lock_guard lock_second{cells_[second].m}; + if (cells_[i].amount < count) { + res = 0; + } else { + cells_[i].amount -= count; + cells_[j].amount += count; + res = 1; + } + } + } + return res; + } + + non_atomic int ReadBoth(int i, int j) { + // debug(stderr, "ReadBoth [%d], [%d] \n", i, j); + int first = std::min(i, j); + int second = std::max(i, j); + int res; + if (first == second) { + std::shared_lock lock_first{cells_[first].m}; + res = cells_[i].amount * 2; + } else { + std::shared_lock lock_first{cells_[first].m}; + { + std::shared_lock lock_second{cells_[second].m}; + res = cells_[i].amount + cells_[j].amount; + } + } + return res; + } +}; + +auto generateAdd(size_t) { + return std::make_tuple(rand() % SIZE, rand() % INIT + 1); +} + +auto generateRead(size_t) { return std::make_tuple(rand() % SIZE); } + +auto generateTransfer(size_t) { + return std::make_tuple(rand() % SIZE, rand() % SIZE, + rand() % INIT + 1); +} + +auto generateReadBoth(size_t) { + return std::make_tuple(rand() % SIZE, rand() % SIZE); +} + +using spec_t = ltest::Spec; + +LTEST_ENTRYPOINT(spec_t); + +target_method(generateAdd, void, Bank, Add, int, size_t); +target_method(generateRead, int, Bank, Read, int); +target_method(generateTransfer, int, Bank, Transfer, int, int, size_t); +target_method(generateReadBoth, int, Bank, ReadBoth, int, int); \ No newline at end of file diff --git a/verifying/blocking/bank_deadlock.cpp b/verifying/blocking/bank_deadlock.cpp new file mode 100644 index 00000000..940489d8 --- /dev/null +++ b/verifying/blocking/bank_deadlock.cpp @@ -0,0 +1,99 @@ +#include +#include +#include +#include +#include + +#include "runtime/include/verifying.h" +#include "runtime/include/verifying_macro.h" +#include "verifying/specs/bank.h" + +class Bank { + private: + struct Cell { + std::shared_mutex m; + int amount; + Cell(int amount) : amount(amount) {} + }; + + std::deque cells_; + + public: + Bank() { + for (int i = 0; i < SIZE; ++i) { + cells_.emplace_back(INIT / 2); + } + } + + non_atomic int Add(int i, size_t count) { + std::lock_guard lock{cells_[i].m}; + cells_[i].amount += count; + return 0; + } + + non_atomic int Read(int i) { + std::shared_lock lock{cells_[i].m}; + return cells_[i].amount; + } + + non_atomic int Transfer(int i, int j, size_t count) { + int res; + if (i == j) { + std::shared_lock lock_first{cells_[i].m}; + res = count <= cells_[i].amount; + } else { + std::lock_guard lock_first{cells_[i].m}; + { + std::lock_guard lock_second{cells_[j].m}; + if (cells_[i].amount < count) { + res = 0; + } else { + cells_[i].amount -= count; + cells_[j].amount += count; + res = 1; + } + } + } + return res; + } + + non_atomic int ReadBoth(int i, int j) { + int res; + if (i == j) { + std::shared_lock lock_first{cells_[i].m}; + res = cells_[i].amount * 2; + } else { + std::shared_lock lock_first{cells_[i].m}; + { + std::shared_lock lock_second{cells_[j].m}; + res = cells_[i].amount + cells_[j].amount; + } + } + return res; + } +}; + +auto generateAdd(size_t) { + return std::make_tuple(rand() % SIZE, rand() % INIT + 1); +} + +auto generateRead(size_t) { return std::make_tuple(rand() % SIZE); } + +auto generateTransfer(size_t) { + return std::make_tuple(rand() % SIZE, rand() % SIZE, + rand() % INIT + 1); +} + +auto generateReadBoth(size_t) { + return std::make_tuple(rand() % SIZE, rand() % SIZE); +} + +using spec_t = ltest::Spec; + +LTEST_ENTRYPOINT(spec_t); + +target_method(generateAdd, int, Bank, Add, int, size_t); +target_method(generateRead, int, Bank, Read, int); +target_method(generateTransfer, int, Bank, Transfer, int, int, size_t); +target_method(generateReadBoth, int, Bank, ReadBoth, int, int); \ No newline at end of file diff --git a/verifying/blocking/buffered_channel.cpp b/verifying/blocking/buffered_channel.cpp new file mode 100644 index 00000000..c8cc3b1b --- /dev/null +++ b/verifying/blocking/buffered_channel.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include + +#include "../specs/queue.h" +#include "runtime/include/value_wrapper.h" +#include "runtime/include/verifying.h" +#include "verifiers/buffered_channel_verifier.h" + +constexpr int N = 5; + +namespace spec { +struct BufferedChannel { + void Send(int v) { deq.push_back(v); } + + int Recv() { + if (deq.empty()) { + return -1; + } + auto value = deq.front(); + deq.pop_front(); + return value; + } + + using method_t = std::function; + static auto GetMethods() { + method_t send_func = [](BufferedChannel *l, void *args) { + auto real_args = reinterpret_cast *>(args); + l->Send(std::get<0>(*real_args)); + return void_v; + }; + + method_t recv_func = [](BufferedChannel *l, void *args) -> int { + return l->Recv(); + }; + + return std::map{ + {"Send", send_func}, + {"Recv", recv_func}, + }; + } + + std::deque deq; +}; + +struct BufferedChannelHash { + size_t operator()(const BufferedChannel &r) const { + int res = 0; + for (int elem : r.deq) { + res += elem; + } + return res; + } +}; + +struct BufferedChannelEquals { + bool operator()(const BufferedChannel &lhs, + const BufferedChannel &rhs) const { + return lhs.deq == rhs.deq; + } +}; +}; // namespace spec + +struct BufferedChannel { + non_atomic void Send(int v) { + std::unique_lock lock{mutex_}; + while (!closed_ && full_) { + debug(stderr, "Waiting in send...\n"); + send_side_cv_.wait(lock); + } + debug(stderr, "Send\n"); + + queue_[sidx_] = v; + sidx_ = (sidx_ + 1) % N; + full_ = (sidx_ == ridx_); + empty_ = false; + recv_side_cv_.notify_one(); + } + + non_atomic int Recv() { + std::unique_lock lock{mutex_}; + while (!closed_ && empty_) { + debug(stderr, "Waiting in recv...\n"); + recv_side_cv_.wait(lock); + } + debug(stderr, "Recv\n"); + auto val = queue_[ridx_]; + ridx_ = (ridx_ + 1) % 5; + empty_ = (sidx_ == ridx_); + full_ = false; + send_side_cv_.notify_one(); + return val; + } + + void Close() { + closed_.store(true); + send_side_cv_.notify_all(); + recv_side_cv_.notify_all(); + } + + std::mutex mutex_; + std::condition_variable send_side_cv_, recv_side_cv_; + std::atomic closed_{false}; + + bool full_{false}; + bool empty_{true}; + + uint32_t sidx_{0}, ridx_{0}; + + std::array queue_{}; +}; + +auto generateInt(size_t) { + return ltest::generators::makeSingleArg(rand() % 10 + 1); +} + +using spec_t = + ltest::Spec; + +LTEST_ENTRYPOINT_CONSTRAINT(spec_t, spec::BufferedChannelVerifier); + +target_method(generateInt, void, BufferedChannel, Send, int); +target_method(ltest::generators::genEmpty, int, BufferedChannel, Recv); diff --git a/verifying/blocking/folly_flatcombining_queue.cpp b/verifying/blocking/folly_flatcombining_queue.cpp new file mode 100644 index 00000000..6a1075c1 --- /dev/null +++ b/verifying/blocking/folly_flatcombining_queue.cpp @@ -0,0 +1,34 @@ +#include + +#include "runtime/include/generators.h" +#include "runtime/include/verifying.h" +#include "runtime/include/verifying_macro.h" +#include "verifying/specs/queue.h" + +class FlatCombiningQueue : public folly::FlatCombining { + spec::Queue<> queue_; + + public: + non_atomic void Push(int v) { + this->requestFC([&]() { queue_.Push(v); }); + debug(stderr, "Push %d completed\n", v); + } + non_atomic int Pop() { + int result; + this->requestFC([&]() { result = queue_.Pop(); }); + debug(stderr, "Pop completed\n"); + return result; + } +}; + +auto generateInt(size_t thread_num) { + return std::make_tuple(rand() % 10 + 1); +} + +using spec_t = ltest::Spec, spec::QueueHash, + spec::QueueEquals>; + +LTEST_ENTRYPOINT(spec_t); + +target_method(generateInt, void, FlatCombiningQueue, Push, int); +target_method(ltest::generators::genEmpty, int, FlatCombiningQueue, Pop); diff --git a/verifying/blocking/folly_rwspinlock.cpp b/verifying/blocking/folly_rwspinlock.cpp index caece8f3..8e2c390d 100644 --- a/verifying/blocking/folly_rwspinlock.cpp +++ b/verifying/blocking/folly_rwspinlock.cpp @@ -1,161 +1,19 @@ -#include -#include - -#include -#include +#include #include "runtime/include/verifying.h" #include "runtime/include/verifying_macro.h" #include "verifying/blocking/verifiers/shared_mutex_verifier.h" #include "verifying/specs/mutex.h" -namespace folly { - -class RWSpinLock { - enum : int32_t { READER = 4, UPGRADED = 2, WRITER = 1 }; - - public: - RWSpinLock() = default; - RWSpinLock(RWSpinLock const&) = delete; - RWSpinLock& operator=(RWSpinLock const&) = delete; - - // Lockable Concept - non_atomic int lock() { - uint_fast32_t count = 0; - while (!LIKELY(try_lock())) { - if (++count > 1000) { - std::this_thread::yield(); - } - } - return 0; - } - - // Writer is responsible for clearing up both the UPGRADED and WRITER bits. - non_atomic int unlock() { - static_assert(READER > WRITER + UPGRADED, "wrong bits!"); - bits_.fetch_and(~(WRITER | UPGRADED), std::memory_order_release); - return 0; - } - - // SharedLockable Concept - non_atomic int lock_shared() { - uint_fast32_t count = 0; - while (!LIKELY(try_lock_shared())) { - if (++count > 1000) { - std::this_thread::yield(); - } - } - return 0; - } - - non_atomic int unlock_shared() { - bits_.fetch_add(-READER, std::memory_order_release); - return 0; - } - - // Downgrade the lock from writer status to reader status. - void unlock_and_lock_shared() { - bits_.fetch_add(READER, std::memory_order_acquire); - unlock(); - } - - // UpgradeLockable Concept - void lock_upgrade() { - uint_fast32_t count = 0; - while (!try_lock_upgrade()) { - if (++count > 1000) { - std::this_thread::yield(); - } - } - } - - void unlock_upgrade() { - bits_.fetch_add(-UPGRADED, std::memory_order_acq_rel); - } - - // unlock upgrade and try to acquire write lock - void unlock_upgrade_and_lock() { - int64_t count = 0; - while (!try_unlock_upgrade_and_lock()) { - if (++count > 1000) { - std::this_thread::yield(); - } - } - } - - // unlock upgrade and read lock atomically - void unlock_upgrade_and_lock_shared() { - bits_.fetch_add(READER - UPGRADED, std::memory_order_acq_rel); - } - - // write unlock and upgrade lock atomically - void unlock_and_lock_upgrade() { - // need to do it in two steps here -- as the UPGRADED bit might be OR-ed at - // the same time when other threads are trying do try_lock_upgrade(). - bits_.fetch_or(UPGRADED, std::memory_order_acquire); - bits_.fetch_add(-WRITER, std::memory_order_release); - } - - // Attempt to acquire writer permission. Return false if we didn't get it. - bool try_lock() { - int32_t expect = 0; - return bits_.compare_exchange_strong(expect, WRITER, - std::memory_order_acq_rel); - } - - // Try to get reader permission on the lock. This can fail if we - // find out someone is a writer or upgrader. - // Setting the UPGRADED bit would allow a writer-to-be to indicate - // its intention to write and block any new readers while waiting - // for existing readers to finish and release their read locks. This - // helps avoid starving writers (promoted from upgraders). - bool try_lock_shared() { - // fetch_add is considerably (100%) faster than compare_exchange, - // so here we are optimizing for the common (lock success) case. - int32_t value = bits_.fetch_add(READER, std::memory_order_acquire); - if (FOLLY_UNLIKELY(value & (WRITER | UPGRADED))) { - bits_.fetch_add(-READER, std::memory_order_release); - return false; - } - return true; - } - - // try to unlock upgrade and write lock atomically - bool try_unlock_upgrade_and_lock() { - int32_t expect = UPGRADED; - return bits_.compare_exchange_strong(expect, WRITER, - std::memory_order_acq_rel); - } - - // try to acquire an upgradable lock. - bool try_lock_upgrade() { - int32_t value = bits_.fetch_or(UPGRADED, std::memory_order_acquire); - - // Note: when failed, we cannot flip the UPGRADED bit back, - // as in this case there is either another upgrade lock or a write lock. - // If it's a write lock, the bit will get cleared up when that lock's done - // with unlock(). - return ((value & (UPGRADED | WRITER)) == 0); - } - - // mainly for debugging purposes. - int32_t bits() const { return bits_.load(std::memory_order_acquire); } - - void Reset() { bits_.store(0); } - - private: - std::atomic bits_{0}; -}; - -} // namespace folly using spec_t = -ltest::Spec; + ltest::Spec; -LTEST_ENTRYPOINT_CONSTRAINT(spec_t, SharedMutexVerifier); +LTEST_ENTRYPOINT_CONSTRAINT(spec_t, spec::SharedMutexVerifier); target_method(ltest::generators::genEmpty, int, folly::RWSpinLock, lock); target_method(ltest::generators::genEmpty, int, folly::RWSpinLock, lock_shared); target_method(ltest::generators::genEmpty, int, folly::RWSpinLock, unlock); -target_method(ltest::generators::genEmpty, int, folly::RWSpinLock, unlock_shared); \ No newline at end of file +target_method(ltest::generators::genEmpty, int, folly::RWSpinLock, + unlock_shared); diff --git a/verifying/blocking/folly_sharedmutex.cpp b/verifying/blocking/folly_sharedmutex.cpp new file mode 100644 index 00000000..e52e4b01 --- /dev/null +++ b/verifying/blocking/folly_sharedmutex.cpp @@ -0,0 +1,30 @@ +#include + +#include "runtime/include/verifying.h" +#include "runtime/include/verifying_macro.h" +#include "verifying/blocking/verifiers/shared_mutex_verifier.h" +#include "verifying/specs/mutex.h" + +using spec_t = + ltest::Spec; + +LTEST_ENTRYPOINT_CONSTRAINT(spec_t, spec::SharedMutexVerifier); + +target_method(ltest::generators::genEmpty, int, folly::SharedMutex, lock); + +target_method(ltest::generators::genEmpty, int, folly::SharedMutex, unlock); + +int (folly::SharedMutexImpl::*lock_shared)() = + &folly::SharedMutexImpl::lock_shared; + +const char *lock_shared_task_name = "lock_shared"; +ltest::TargetMethod lock_shared_ltest_method_cls{ + lock_shared_task_name, ltest::generators::genEmpty, lock_shared}; + +int (folly::SharedMutexImpl::*unlock_shared)() = + &folly::SharedMutexImpl::unlock_shared; + +const char *unlock_shared_task_name = "unlock_shared"; +ltest::TargetMethod unlock_shared_ltest_method_cls{ + unlock_shared_task_name, ltest::generators::genEmpty, unlock_shared}; diff --git a/verifying/blocking/mutexed_register.cpp b/verifying/blocking/mutexed_register.cpp index c9d38f44..4bd1fa40 100644 --- a/verifying/blocking/mutexed_register.cpp +++ b/verifying/blocking/mutexed_register.cpp @@ -1,26 +1,20 @@ #include -#include "folly/synchronization/Lock.h" #include "runtime/include/verifying.h" #include "verifying/specs/register.h" -typedef folly::detail::lock_base_unique lock_guard; - struct Register { non_atomic void add() { - lock_guard lock{m_}; + while (!m_.try_lock()) { + } ++x_; + m_.unlock(); } non_atomic int get() { - lock_guard lock{m_}; + std::lock_guard lock{m_}; return x_; } - void Reset() { - lock_guard lock{m_}; - x_ = 0; - } - int x_{}; std::mutex m_; }; @@ -33,4 +27,4 @@ LTEST_ENTRYPOINT(spec_t); target_method(ltest::generators::genEmpty, void, Register, add); -target_method(ltest::generators::genEmpty, int, Register, get); \ No newline at end of file +target_method(ltest::generators::genEmpty, int, Register, get); diff --git a/verifying/blocking/nonlinear_mutex.cpp b/verifying/blocking/nonlinear_mutex.cpp index 2c8bedc8..bb65d99d 100644 --- a/verifying/blocking/nonlinear_mutex.cpp +++ b/verifying/blocking/nonlinear_mutex.cpp @@ -50,8 +50,6 @@ class Mutex { return 0; } - void Reset() { locked_.store(0); } - private: std::atomic_int32_t locked_{0}; }; @@ -62,5 +60,4 @@ using spec_t = ltest::Spec +#include + +#include "../specs/register.h" +#include "runtime/include/blocking_primitives.h" +#include "runtime/include/verifying.h" +#include "runtime/include/verifying_macro.h" + +struct MutexedRegister { + public: + non_atomic int add() { + std::unique_lock lock{m}; + x++; + return 0; + } + + non_atomic int get() { + std::shared_lock lock{m}; + return x; + } + + private: + int x{}; + std::shared_mutex m{}; +}; + +using spec_t = + ltest::Spec; + +LTEST_ENTRYPOINT(spec_t); + +target_method(ltest::generators::genEmpty, int, MutexedRegister, add); +target_method(ltest::generators::genEmpty, int, MutexedRegister, get); diff --git a/verifying/blocking/simple_deadlock.cpp b/verifying/blocking/simple_deadlock.cpp new file mode 100644 index 00000000..020f888b --- /dev/null +++ b/verifying/blocking/simple_deadlock.cpp @@ -0,0 +1,87 @@ +#include +#include + +#include "runtime/include/generators.h" +#include "runtime/include/value_wrapper.h" +#include "runtime/include/verifying.h" +#include "runtime/include/verifying_macro.h" + +namespace spec { + +struct MutexDeadlock { + void LockFirstSecond() { + x1 -= 1; + x2 += 1; + } + + void LockSecondFirst() { + x2 -= 1; + x1 += 1; + } + + int x1{0}, x2{0}; + + using method_t = std::function; + static auto GetMethods() { + method_t fs_func = [](MutexDeadlock *l, void *args) -> ValueWrapper { + l->LockFirstSecond(); + return void_v; + }; + + method_t sf_func = [](MutexDeadlock *l, void *args) -> ValueWrapper { + l->LockSecondFirst(); + return void_v; + }; + + return std::map{ + {"LockFirstSecond", fs_func}, + {"LockSecondFirst", sf_func}, + }; + } +}; + +struct MutexDeadlockHash { + size_t operator()(const MutexDeadlock &r) const { return r.x1 ^ r.x2; } +}; + +struct MutexDeadlockEquals { + bool operator()(const MutexDeadlock &lhs, const MutexDeadlock &rhs) const { + return lhs.x1 == rhs.x1 && lhs.x2 == rhs.x2; + } +}; +} // namespace spec + +struct MutexDeadlock { + non_atomic void LockFirstSecond() { + std::lock_guard g1{m1_}; + x1 -= 1; + { + std::lock_guard g2{m2_}; + x2 += 1; + } + } + + non_atomic void LockSecondFirst() { + std::lock_guard g2{m2_}; + x2 -= 1; + { + std::lock_guard g1{m1_}; + x1 += 1; + } + } + + int x1{0}, x2{0}; + + std::mutex m1_; + std::mutex m2_; +}; + +using spec_t = ltest::Spec; + +LTEST_ENTRYPOINT(spec_t); + +target_method(ltest::generators::genEmpty, void, MutexDeadlock, + LockFirstSecond); +target_method(ltest::generators::genEmpty, void, MutexDeadlock, + LockSecondFirst); \ No newline at end of file diff --git a/verifying/blocking/simple_mutex.cpp b/verifying/blocking/simple_mutex.cpp index 65629310..74b3f1e5 100644 --- a/verifying/blocking/simple_mutex.cpp +++ b/verifying/blocking/simple_mutex.cpp @@ -38,6 +38,7 @@ class Mutex { while (CompareExchange(0, 2) != 0) { if (CompareExchange(1, 2) > 0) { while (locked_.load() == 2) { + debug(stderr, "Futex wait spinlock\n"); FutexWait(Addr(locked_), 2); } } @@ -56,8 +57,6 @@ class Mutex { return 0; } - void Reset() { locked_.store(0); } - private: std::atomic_int32_t locked_{0}; }; @@ -68,5 +67,4 @@ using spec_t = ltest::Spec + +#include "runtime/include/lib.h" + +namespace spec { +struct BufferedChannelVerifier { + bool Verify(const std::string& task_name, size_t thread_id) { + if (task_name == "Send") { + if (senders_ == 0) { + ++senders_; + ++size_; + return true; + } + return false; + } else if (task_name == "Recv") { + if (receivers_ == 0) { + ++receivers_; + if (size_ > 0) { + --size_; + } + return true; + } + return false; + } else { + assert(false); + } + } + + void OnFinished(Task& task, size_t thread_id) { + auto task_name = task->GetName(); + if (task_name == "Send") { + --senders_; + return; + } else if (task_name == "Recv") { + --receivers_; + return; + } else { + assert(false); + } + } + + std::optional ReleaseTask(size_t thread_id) { + if (senders_ > 0) { + return {"Recv"}; + } + return std::nullopt; + } + + size_t senders_; + size_t receivers_; + size_t size_; +}; +} // namespace spec \ No newline at end of file diff --git a/verifying/blocking/verifiers/mutex_verifier.h b/verifying/blocking/verifiers/mutex_verifier.h index cef4f2b4..30f12cc6 100644 --- a/verifying/blocking/verifiers/mutex_verifier.h +++ b/verifying/blocking/verifiers/mutex_verifier.h @@ -3,40 +3,38 @@ #include "runtime/include/scheduler.h" struct MutexVerifier { - bool Verify(CreatedTaskMetaData ctask) { - auto [taskName, is_new, thread_id] = ctask; - debug(stderr, "validating method %s, thread_id: %zu\n", taskName.data(), + bool Verify(const std::string& task_name, size_t thread_id) { + debug(stderr, "validating method %s, thread_id: %zu\n", task_name.data(), thread_id); - if (!is_new) { - return true; - } if (status.count(thread_id) == 0) { status[thread_id] = 0; } - if (taskName == "Lock") { + if (task_name == "Lock") { return status[thread_id] == 0; - } else if (taskName == "Unlock") { + } else if (task_name == "Unlock") { return status[thread_id] == 1; } else { assert(false); } } - void OnFinished(TaskWithMetaData ctask) { - auto [task, is_new, thread_id] = ctask; - auto taskName = task->GetName(); - debug(stderr, "On finished method %s, thread_id: %zu\n", taskName.data(), + void OnFinished(Task& task, size_t thread_id) { + auto task_name = task->GetName(); + debug(stderr, "On finished method %s, thread_id: %zu\n", task_name.data(), thread_id); - if (taskName == "Lock") { + if (task_name == "Lock") { status[thread_id] = 1; - } else if (taskName == "Unlock") { + } else if (task_name == "Unlock") { status[thread_id] = 0; } } - void Reset() { status.clear(); } - - void UpdateState(std::string_view, int, bool){} + std::optional ReleaseTask(size_t thread_id) { + if (status[thread_id] == 1) { + return {"Unlock"}; + } + return std::nullopt; + } // NOTE(kmitkin): we cannot just store number of thread that holds mutex // because Lock can finish before Unlock! diff --git a/verifying/blocking/verifiers/shared_mutex_verifier.h b/verifying/blocking/verifiers/shared_mutex_verifier.h index b2d34ba7..628fdbf0 100644 --- a/verifying/blocking/verifiers/shared_mutex_verifier.h +++ b/verifying/blocking/verifiers/shared_mutex_verifier.h @@ -2,52 +2,58 @@ #include "runtime/include/scheduler.h" +namespace spec { + struct SharedMutexVerifier { enum : int32_t { READER = 4, WRITER = 1, FREE = 0 }; /// Verify checks the state of a mutex on starting of `ctask` - bool Verify(CreatedTaskMetaData ctask) { - auto [taskName, is_new, thread_id] = ctask; - debug(stderr, "validating method %s, thread_id: %zu\n", taskName.data(), + bool Verify(const std::string& task_name, size_t thread_id) { + debug(stderr, "validating method %s, thread_id: %zu\n", task_name.data(), thread_id); if (status.count(thread_id) == 0) { status[thread_id] = FREE; } /// When `lock` is executed, it is expected that current thread doesn't hold /// a mutex because otherwise we get recursive lock and UB - if (taskName == "lock") { + if (task_name == "lock") { return status[thread_id] == FREE; - } else if (taskName == "unlock") { + } else if (task_name == "unlock") { return status[thread_id] == WRITER; - } else if (taskName == "lock_shared") { + } else if (task_name == "lock_shared") { return status[thread_id] == FREE; - } else if (taskName == "unlock_shared") { + } else if (task_name == "unlock_shared") { return status[thread_id] == READER; } else { assert(false); } } - void OnFinished(TaskWithMetaData ctask) { - auto [task, is_new, thread_id] = ctask; - auto taskName = task->GetName(); - debug(stderr, "On finished method %s, thread_id: %zu\n", taskName.data(), + void OnFinished(Task& task, size_t thread_id) { + auto task_name = task->GetName(); + debug(stderr, "On finished method %s, thread_id: %zu\n", task_name.data(), thread_id); - if (taskName == "lock") { + if (task_name == "lock") { status[thread_id] = WRITER; - } else if (taskName == "unlock") { + } else if (task_name == "unlock") { status[thread_id] = FREE; - } else if (taskName == "lock_shared") { + } else if (task_name == "lock_shared") { status[thread_id] = READER; - } else if (taskName == "unlock_shared") { + } else if (task_name == "unlock_shared") { status[thread_id] = FREE; } else { assert(false); } } - void Reset() { status.clear(); } - - void UpdateState(std::string_view, int, bool) {} + std::optional ReleaseTask(size_t thread_id) { + if (status[thread_id] == WRITER) { + return {"unlock"}; + } else if (status[thread_id] == READER) { + return {"unlock_shared"}; + } + return std::nullopt; + } std::unordered_map status; }; +} // namespace spec diff --git a/verifying/lib/mutex.h b/verifying/lib/mutex.h deleted file mode 100644 index 40536dc0..00000000 --- a/verifying/lib/mutex.h +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include -#include - -#include "../../runtime/include/verifying.h" - -// Mutex operates with ltest token. -// After unsuccessful Lock() it parks the method. -// Ltest runtime doesn't launch parked tasks. -// It's important to call `CoroYield()` implicitly after possible parking. -struct Mutex { - // First - holds the lock. - // Others - want the lock. - std::deque> waiters{}; - - Mutex() {} - - // `noexcept` here is needeed to tell clang to generate just `call` instead of - // `invoke`. The full support for the `invoke` will be added later. - void Lock(std::shared_ptr token) noexcept { - waiters.push_back(token); - if (waiters.size() == 1) { - return; - } - token->Park(); - } - - // Assume this method is atomic. - void Unlock() noexcept { - assert(!waiters.empty()); - waiters.pop_front(); - if (!waiters.empty()) { - waiters.front()->Unpark(); - } - } -}; diff --git a/verifying/specs/bank.h b/verifying/specs/bank.h new file mode 100644 index 00000000..83a78069 --- /dev/null +++ b/verifying/specs/bank.h @@ -0,0 +1,88 @@ + + +#include +#include +#include +#include +#include + +#include "runtime/include/value_wrapper.h" +static constexpr size_t INIT = 100; +static constexpr size_t SIZE = 2; + +namespace spec { + +struct LinearBank; + +using bank_method_t = std::function; + +struct LinearBank { + std::deque cells; + + LinearBank() { + for (int i = 0; i < SIZE; ++i) { + cells.emplace_back(INIT / 2); + } + } + + void Add(int i, size_t count) { cells[i] += count; } + + int Read(int i) { return cells[i]; } + + int Transfer(int i, int j, size_t count) { + if (cells[i] < count) { + return 0; + } + cells[i] -= count; + cells[j] += count; + return 1; + } + + int ReadBoth(int i, int j) { return cells[i] + cells[j]; } + + static auto GetMethods() { + bank_method_t add_func = [](LinearBank *l, void *args) -> ValueWrapper { + auto real_args = reinterpret_cast *>(args); + l->Add(std::get<0>(*real_args), std::get<1>(*real_args)); + return void_v; + }; + + bank_method_t read_func = [](LinearBank *l, void *args) -> int { + auto real_args = reinterpret_cast *>(args); + return l->Read(std::get<0>(*real_args)); + }; + + bank_method_t transfer_func = [](LinearBank *l, void *args) -> int { + auto real_args = reinterpret_cast *>(args); + return l->Transfer(std::get<0>(*real_args), std::get<1>(*real_args), + std::get<2>(*real_args)); + }; + + bank_method_t read_both_func = [](LinearBank *l, void *args) -> int { + auto real_args = reinterpret_cast *>(args); + return l->ReadBoth(std::get<0>(*real_args), std::get<1>(*real_args)); + }; + + return std::map{{"Add", add_func}, + {"Read", read_func}, + {"Transfer", transfer_func}, + {"ReadBoth", read_both_func}}; + } +}; + +struct LinearBankHash { + size_t operator()(const LinearBank &r) const { + size_t hash = 0; + for (auto cell : r.cells) { + hash += cell; + } + return hash; + } +}; + +struct LinearBankEquals { + bool operator()(const LinearBank &lhs, const LinearBank &rhs) const { + return lhs.cells == rhs.cells; + } +}; +} // namespace spec diff --git a/verifying/specs/bounded_queue.h b/verifying/specs/bounded_queue.h deleted file mode 100644 index 274fdddc..00000000 --- a/verifying/specs/bounded_queue.h +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include -#include -#include -#include - -#include "../../runtime/include/verifying.h" -#include "runtime/include/value_wrapper.h" - -namespace spec { - -struct Queue; - -using mutex_method_t = std::function; - -const int size = 2; - -struct Queue { - std::deque deq{}; - - int Push(int v) { - if (deq.size() >= size) { - return 0; - } - deq.push_back(v); - return 1; - } - int Pop() { - if (deq.empty()) return 0; - int res = deq.front(); - deq.pop_front(); - return res; - } - static auto GetMethods() { - mutex_method_t push_func = [](Queue *l, void *args) -> int { - auto real_args = reinterpret_cast *>(args); - return l->Push(std::get<0>(*real_args)); - }; - - mutex_method_t pop_func = [](Queue *l, void *args) -> int { - return l->Pop(); - }; - - return std::map{ - {"Push", push_func}, - {"Pop", pop_func}, - }; - } -}; - -struct QueueHash { - size_t operator()(const Queue &r) const { - int res = 0; - for (int elem : r.deq) { - res += elem; - } - return res; - } -}; - -struct QueueEquals { - bool operator()(const Queue &lhs, const Queue &rhs) const { - return lhs.deq == rhs.deq; - } -}; - -} // namespace spec diff --git a/verifying/specs/mutex.h b/verifying/specs/mutex.h index 3b97f19d..d75956c9 100644 --- a/verifying/specs/mutex.h +++ b/verifying/specs/mutex.h @@ -2,6 +2,7 @@ #include #include #include + #include "runtime/include/value_wrapper.h" namespace spec { @@ -55,7 +56,8 @@ struct LinearMutexEquals { struct SharedLinearMutex; -using shared_mutex_method_t = std::function; +using shared_mutex_method_t = + std::function; struct SharedLinearMutex { private: diff --git a/verifying/specs/queue.h b/verifying/specs/queue.h index c6111262..1a536477 100644 --- a/verifying/specs/queue.h +++ b/verifying/specs/queue.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "../../runtime/include/verifying.h" #include "runtime/include/value_wrapper.h" @@ -11,9 +12,7 @@ namespace spec { template , std::size_t ValueIndex = 0> struct Queue { std::deque deq{}; - void Push(int v) { - deq.push_back(v); - } + void Push(int v) { deq.push_back(v); } int Pop() { if (deq.empty()) return 0; int res = deq.front(); @@ -38,21 +37,19 @@ struct Queue { } }; -template > struct QueueHash { - size_t operator()(const QueueCls &r) const { + size_t operator()(const Queue<> &r) const { int res = 0; for (int elem : r.deq) { res += elem; } return res; - } // namespace spec + } }; -template > struct QueueEquals { template - bool operator()(const QueueCls &lhs, const QueueCls &rhs) const { + bool operator()(const Queue<> &lhs, const Queue<> &rhs) const { return lhs.deq == rhs.deq; } }; diff --git a/verifying/specs/register.h b/verifying/specs/register.h index 05646c77..e2baf926 100644 --- a/verifying/specs/register.h +++ b/verifying/specs/register.h @@ -2,13 +2,15 @@ #include #include #include + #include "runtime/include/value_wrapper.h" namespace spec { struct LinearRegister; -using mutex_method_t = std::function; +using register_method_t = + std::function; struct LinearRegister { int x = 0; @@ -16,16 +18,16 @@ struct LinearRegister { int get() { return x; } static auto GetMethods() { - mutex_method_t add_func = [](LinearRegister *l, void *) { + register_method_t add_func = [](LinearRegister *l, void *) { l->add(); return void_v; }; - mutex_method_t get_func = [](LinearRegister *l, void *) { + register_method_t get_func = [](LinearRegister *l, void *) { return l->get(); }; - return std::map{ + return std::map{ {"add", add_func}, {"get", get_func}, }; diff --git a/verifying/targets/CMakeLists.txt b/verifying/targets/CMakeLists.txt index 6e8d2023..fa5e8a79 100644 --- a/verifying/targets/CMakeLists.txt +++ b/verifying/targets/CMakeLists.txt @@ -1,8 +1,6 @@ set (SOURCE_TARGET_LIST atomic_register.cpp - deadlock.cpp fast_queue.cpp - mutex_queue.cpp race_register.cpp nonlinear_queue.cpp nonlinear_set.cpp @@ -73,6 +71,10 @@ add_integration_test("nonlinear_set_pct_minimization" "verify" TRUE nonlinear_set --tasks 40 --rounds 1000000 --strategy pct --minimize ) +add_integration_test("nonlinear_queue_pct" "verify" TRUE + nonlinear_queue --rounds 10000 --strategy pct +) + add_integration_test("unique_args" "verify" FALSE unique_args ) diff --git a/verifying/targets/atomic_register.cpp b/verifying/targets/atomic_register.cpp index f328f7c5..d5a8e0dd 100644 --- a/verifying/targets/atomic_register.cpp +++ b/verifying/targets/atomic_register.cpp @@ -1,5 +1,6 @@ /** - * ./build/verifying/targets/atomic_register --tasks 3 --strategy tla --rounds 100000 + * ./build/verifying/targets/atomic_register --tasks 3 --strategy tla --rounds + * 100000 */ #include @@ -10,8 +11,6 @@ struct Register { non_atomic void add() { x.fetch_add(1); } non_atomic int get() { return x.load(); } - void Reset() { x.store(0); } - std::atomic x{}; }; @@ -22,5 +21,4 @@ using spec_t = LTEST_ENTRYPOINT(spec_t); target_method(ltest::generators::genEmpty, void, Register, add); - -target_method(ltest::generators::genEmpty, int, Register, get); \ No newline at end of file +target_method(ltest::generators::genEmpty, int, Register, get); diff --git a/verifying/targets/counique_args.cpp b/verifying/targets/counique_args.cpp index b599640e..760b6ca5 100644 --- a/verifying/targets/counique_args.cpp +++ b/verifying/targets/counique_args.cpp @@ -8,7 +8,6 @@ struct Promise; - // NOLINTBEGIN(readability-identifier-naming) struct Coroutine : std::coroutine_handle { using promise_type = ::Promise; @@ -31,7 +30,7 @@ Coroutine CoFun(int i) { co_return; } struct CoUniqueArgsTest { - CoUniqueArgsTest() {} + CoUniqueArgsTest() { Reset(); } ValueWrapper Get(size_t i) { assert(!used[i]); used[i] = true; diff --git a/verifying/targets/deadlock.cpp b/verifying/targets/deadlock.cpp deleted file mode 100644 index 4d87eeb0..00000000 --- a/verifying/targets/deadlock.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/** - * ./build/verifying/targets/deadlock -v --tasks 5 --strategy rr - * ./build/verifying/targets/deadlock -v --tasks 5 --strategy random - * - * It important to limit switches. - * ./build/verifying/targets/deadlock -v --tasks 2 --strategy tla --rounds 100000 --switches 4 - */ -#include -#include - -#include "../lib/mutex.h" - -// Test is implementation and the specification at the same time. -struct Test { - Test() {} - - // Lock(odd) in parallel with Lock(even) causes deadlock. - non_atomic void Lock(std::shared_ptr token, int v) { - if (v % 2 == 0) { - mu1.Lock(token); - CoroYield(); - mu2.Lock(token); - CoroYield(); - } else { - mu2.Lock(token); - CoroYield(); - mu1.Lock(token); - CoroYield(); - } - mu1.Unlock(); - mu2.Unlock(); - } - - void Reset() { - mu1 = Mutex{}; - mu2 = Mutex{}; - } - - Mutex mu1, mu2{}; - - using method_t = std::function; - - static auto GetMethods() { - method_t lock_func = [](Test *l, void *args) -> int { - // `void` return type is always return 0 equivalent. - return 0; - }; - - return std::map{ - {"Lock", lock_func}, - }; - } -}; - -auto generateInt(size_t thread_num) { - return ltest::generators::makeSingleArg(static_cast(thread_num)); -} - -auto generateArgs(size_t thread_num) { - auto token = ltest::generators::genToken(thread_num); - auto _int = generateInt(thread_num); - return std::tuple_cat(token, _int); -} - -using spec_t = ltest::Spec; - -LTEST_ENTRYPOINT(spec_t); - -target_method(generateArgs, void, Test, Lock, std::shared_ptr, int); \ No newline at end of file diff --git a/verifying/targets/fast_queue.cpp b/verifying/targets/fast_queue.cpp index 180a421e..1e40e120 100644 --- a/verifying/targets/fast_queue.cpp +++ b/verifying/targets/fast_queue.cpp @@ -1,11 +1,12 @@ /** - * ./build/verifying/targets/fast_queue --strategy tla --tasks 4 --rounds 50000 --switches 1 + * ./build/verifying/targets/fast_queue --strategy tla --tasks 4 --rounds 50000 + * --switches 1 */ #include #include #include -#include "../specs/bounded_queue.h" +#include "../specs/queue.h" template struct Node { @@ -39,14 +40,6 @@ class MPMCBoundedQueue { } } - void Reset() { - for (size_t i = 0; i < size; ++i) { - vec_[i].generation.store(i); - } - head_.store(0); - tail_.store(0); - } - non_atomic int Push(int value) { while (true) { auto h = head_.load(/*std::memory_order_relaxed*/); @@ -103,11 +96,10 @@ class MPMCBoundedQueue { // POP // 1 == tail + 1? 1 == 1 -using spec_t = ltest::Spec, spec::QueueHash, spec::QueueEquals>; LTEST_ENTRYPOINT(spec_t); target_method(generateInt, int, MPMCBoundedQueue, Push, int); - -target_method(ltest::generators::genEmpty, int, MPMCBoundedQueue, Pop); \ No newline at end of file +target_method(ltest::generators::genEmpty, int, MPMCBoundedQueue, Pop); diff --git a/verifying/targets/mutex_queue.cpp b/verifying/targets/mutex_queue.cpp deleted file mode 100644 index 8fda3395..00000000 --- a/verifying/targets/mutex_queue.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/** - * ./build/verifying/targets/mutex_queue --tasks 4 --switches 1 --rounds 100000 --strategy tla - */ -#include -#include - -#include "../lib/mutex.h" -#include "../specs/queue.h" - -const int N = 100; - -struct Queue { - non_atomic void Push(std::shared_ptr token, int v) { - mutex.Lock(token); - a[head++] = v; - ++cnt; - assert(cnt == 1); - --cnt; - mutex.Unlock(); - } - - non_atomic int Pop(std::shared_ptr token) { - mutex.Lock(token); - int e = 0; - if (head - tail > 0) { - e = a[tail++]; - } - ++cnt; - assert(cnt == 1); - --cnt; - mutex.Unlock(); - return e; - } - - void Reset() { - mutex = Mutex{}; - tail = head = 0; - cnt = 0; - std::fill(a, a + N, 0); - } - - int cnt{}; - Mutex mutex{}; - int tail{}, head{}; - int a[N]{}; -}; - -namespace ltest {} // namespace ltest - -auto generateInt() { return ltest::generators::makeSingleArg(rand() % 10 + 1); } - -auto generateArgs(size_t thread_num) { - auto token = ltest::generators::genToken(thread_num); - auto _int = generateInt(); - return std::tuple_cat(token, _int); -} - -using QueueCls = spec::Queue, int>, 1>; - -using spec_t = ltest::Spec, - spec::QueueEquals>; - -LTEST_ENTRYPOINT(spec_t); - -target_method(generateArgs, void, Queue, Push, std::shared_ptr, int); - -target_method(ltest::generators::genToken, int, Queue, Pop, - std::shared_ptr); diff --git a/verifying/targets/nonlinear_ms_queue.cpp b/verifying/targets/nonlinear_ms_queue.cpp index 06e360ff..d50f7462 100644 --- a/verifying/targets/nonlinear_ms_queue.cpp +++ b/verifying/targets/nonlinear_ms_queue.cpp @@ -110,14 +110,6 @@ struct MSQueue { return value; } - - void Reset() { - // Reset the queue to its initial state - index.store(0); - dummyNode.next.store(nullptr); - head.store(&dummyNode); - tail.store(&dummyNode); - } }; // Arguments generator. @@ -127,7 +119,7 @@ auto generateInt(size_t unused) { // Specify target structure and it's sequential specification. using spec_t = - ltest::Spec, spec::QueueHash<>, spec::QueueEquals<>>; + ltest::Spec, spec::QueueHash, spec::QueueEquals>; LTEST_ENTRYPOINT(spec_t); diff --git a/verifying/targets/nonlinear_queue.cpp b/verifying/targets/nonlinear_queue.cpp index c3f126e2..e77691bf 100644 --- a/verifying/targets/nonlinear_queue.cpp +++ b/verifying/targets/nonlinear_queue.cpp @@ -6,6 +6,8 @@ #include #include "../specs/queue.h" +#include "runtime/include/verifying.h" +#include "runtime/include/verifying_macro.h" const int N = 100; @@ -29,11 +31,6 @@ struct Queue { return 0; } - void Reset() { - head.store(0); - for (int i = 0; i < N; ++i) a[i].store(0); - } - std::atomic a[N]; std::atomic head{}; }; @@ -45,10 +42,10 @@ auto generateInt(size_t unused_param) { // Specify target structure and it's sequential specification. using spec_t = - ltest::Spec, spec::QueueHash<>, spec::QueueEquals<>>; + ltest::Spec, spec::QueueHash, spec::QueueEquals>; LTEST_ENTRYPOINT(spec_t); +// Targets. target_method(generateInt, void, Queue, Push, int); - target_method(ltest::generators::genEmpty, int, Queue, Pop); \ No newline at end of file diff --git a/verifying/targets/nonlinear_set.cpp b/verifying/targets/nonlinear_set.cpp index 2697992a..8a46fd30 100644 --- a/verifying/targets/nonlinear_set.cpp +++ b/verifying/targets/nonlinear_set.cpp @@ -5,7 +5,11 @@ struct SlotsSet { public: - SlotsSet() { Reset(); } + SlotsSet() { + for (size_t i = 0; i < N; ++i) { + slots[i].store(0); + } + } non_atomic int Insert(int value) { assert(value != 0); // zero should never be added @@ -45,12 +49,6 @@ struct SlotsSet { return false; } - void Reset() { - for (size_t i = 0; i < N; ++i) { - slots[i].store(0); - } - } - private: static inline const int N = 100; std::atomic slots[N]; diff --git a/verifying/targets/nonlinear_treiber_stack.cpp b/verifying/targets/nonlinear_treiber_stack.cpp index 66ab8c9d..0833810e 100644 --- a/verifying/targets/nonlinear_treiber_stack.cpp +++ b/verifying/targets/nonlinear_treiber_stack.cpp @@ -3,7 +3,15 @@ #include "../specs/stack.h" struct TreiberStack { - TreiberStack() : nodes(N), head(-1), free_list(0) { Reset(); } + TreiberStack() : nodes(N), head(-1), free_list(0) { + for (size_t i = 0; i < nodes.size() - 1; ++i) { + nodes[i].next.store(i + 1); + } + nodes[nodes.size() - 1].next.store(-1); + + head.store(-1); + free_list.store(0); + } non_atomic void Push(int value) { int node_index; @@ -44,18 +52,6 @@ struct TreiberStack { return value; } - void Reset() { - // Reset free list (each node points to the next) - for (size_t i = 0; i < nodes.size() - 1; ++i) { - nodes[i].next.store(i + 1); - } - nodes[nodes.size() - 1].next.store(-1); - - // Reset stack head and free list pointer - head.store(-1); - free_list.store(0); - } - private: struct Node { int value; diff --git a/verifying/targets/race_register.cpp b/verifying/targets/race_register.cpp index 844f3069..6a1ccb46 100644 --- a/verifying/targets/race_register.cpp +++ b/verifying/targets/race_register.cpp @@ -6,8 +6,6 @@ struct Register { non_atomic int get() { return x; } - void Reset() { x = 0; } - int x{}; }; @@ -19,4 +17,4 @@ LTEST_ENTRYPOINT(spec_t); target_method(ltest::generators::genEmpty, void, Register, add); -target_method(ltest::generators::genEmpty, int, Register, get); \ No newline at end of file +target_method(ltest::generators::genEmpty, int, Register, get); diff --git a/verifying/targets/unique_args.cpp b/verifying/targets/unique_args.cpp index ee53dc8d..8d71a921 100644 --- a/verifying/targets/unique_args.cpp +++ b/verifying/targets/unique_args.cpp @@ -9,7 +9,7 @@ static std::vector used(limit, false); static std::vector done(limit, false); struct CoUniqueArgsTest { - CoUniqueArgsTest() {} + CoUniqueArgsTest() { Reset(); } ValueWrapper Get(size_t i) { assert(!used[i]); used[i] = true; From 2d8f2ef9898f3cde51d2e1f3e19bb4bcbce14145 Mon Sep 17 00:00:00 2001 From: Kirill Mitkin Date: Fri, 16 May 2025 00:57:03 +0000 Subject: [PATCH 2/5] refactor code --- runtime/CMakeLists.txt | 3 +- runtime/coro_ctx_guard.cpp | 13 ++++++ runtime/include/block_manager.h | 10 ++--- runtime/include/coro_ctx_guard.h | 12 ++++++ runtime/include/lib.h | 4 -- runtime/include/scheduler.h | 4 +- runtime/include/syscall_trap.h | 12 ------ runtime/include/verifying.h | 2 - runtime/include/yield_guard.h | 12 ------ runtime/lib.cpp | 8 ++-- runtime/minimization.cpp | 3 +- runtime/syscall_trap.cpp | 9 ---- runtime/yield_guard.cpp | 9 ---- syscall_intercept/hook.cpp | 74 +++++++++++++++++--------------- 14 files changed, 78 insertions(+), 97 deletions(-) create mode 100644 runtime/coro_ctx_guard.cpp create mode 100644 runtime/include/coro_ctx_guard.h delete mode 100644 runtime/include/syscall_trap.h delete mode 100644 runtime/include/yield_guard.h delete mode 100644 runtime/syscall_trap.cpp delete mode 100644 runtime/yield_guard.cpp diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 61d04054..565ccc47 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -6,10 +6,9 @@ set (SOURCE_FILES pretty_printer.cpp verifying.cpp generators.cpp - syscall_trap.cpp minimization.cpp minimization_smart.cpp - yield_guard.cpp + coro_ctx_guard.cpp ) add_library(runtime SHARED ${SOURCE_FILES}) diff --git a/runtime/coro_ctx_guard.cpp b/runtime/coro_ctx_guard.cpp new file mode 100644 index 00000000..2435db4f --- /dev/null +++ b/runtime/coro_ctx_guard.cpp @@ -0,0 +1,13 @@ +#include "coro_ctx_guard.h" + +/// True in coroutine contexts +/// required for +/// 1. incapsulating CoroYield calls, allowing +/// to call methods annotated with non_atomic in scheduler fiber +/// 2. incapsulating syscall hook + +bool ltest_coro_ctx = 0; + +ltest::CoroCtxGuard::CoroCtxGuard() { ltest_coro_ctx = true; } + +ltest::CoroCtxGuard::~CoroCtxGuard() { ltest_coro_ctx = false; } diff --git a/runtime/include/block_manager.h b/runtime/include/block_manager.h index 0c348528..b476d347 100644 --- a/runtime/include/block_manager.h +++ b/runtime/include/block_manager.h @@ -13,20 +13,20 @@ struct BlockManager { // table & linked list std::unordered_map> queues; - void BlockOn(BlockState state, CoroBase *coro) { + inline void BlockOn(BlockState state, CoroBase *coro) { if (!queues.contains(state.addr)) { queues[state.addr] = std::deque{}; } queues[state.addr].push_back(coro); } - bool IsBlocked(const BlockState &state, CoroBase *coro) { + inline bool IsBlocked(const BlockState &state, CoroBase *coro) { return state.addr && std::find(queues[state.addr].begin(), queues[state.addr].end(), coro) != queues[state.addr].end(); } - std::size_t UnblockOn(std::intptr_t addr, std::size_t max_wakes) { + inline std::size_t UnblockOn(std::intptr_t addr, std::size_t max_wakes) { if (!queues.contains(addr)) [[unlikely]] { return 0; } @@ -38,7 +38,7 @@ struct BlockManager { return wakes; } - void UnblockAllOn(std::intptr_t addr) { + inline void UnblockAllOn(std::intptr_t addr) { if (!queues.contains(addr)) { return; } @@ -46,4 +46,4 @@ struct BlockManager { } }; -extern BlockManager block_manager; +inline BlockManager block_manager; diff --git a/runtime/include/coro_ctx_guard.h b/runtime/include/coro_ctx_guard.h new file mode 100644 index 00000000..d505992a --- /dev/null +++ b/runtime/include/coro_ctx_guard.h @@ -0,0 +1,12 @@ +#pragma once + +extern bool ltest_coro_ctx; + +namespace ltest { + +struct CoroCtxGuard { + CoroCtxGuard(); + ~CoroCtxGuard(); +}; + +} // namespace ltest \ No newline at end of file diff --git a/runtime/include/lib.h b/runtime/include/lib.h index 5901e049..c3d3b99a 100644 --- a/runtime/include/lib.h +++ b/runtime/include/lib.h @@ -18,8 +18,6 @@ struct CoroBase; struct CoroutineStatus; -struct BlockManager; - // Current executing coroutine. extern std::shared_ptr this_coro; @@ -28,8 +26,6 @@ extern boost::context::fiber_context sched_ctx; extern std::optional coroutine_status; -extern BlockManager block_manager; - struct CoroutineStatus { std::string_view name; bool has_started; diff --git a/runtime/include/scheduler.h b/runtime/include/scheduler.h index 81f47ebf..86420377 100644 --- a/runtime/include/scheduler.h +++ b/runtime/include/scheduler.h @@ -30,7 +30,9 @@ struct TaskWithMetaData { /// UB. template concept StrategyTaskVerifier = requires(T a) { - { a.Verify(std::declval(), size_t()) } -> std::same_as; + { + a.Verify(std::declval(), size_t()) + } -> std::same_as; { a.OnFinished(std::declval(), size_t()) } -> std::same_as; { a.ReleaseTask(size_t()) } -> std::same_as>; }; diff --git a/runtime/include/syscall_trap.h b/runtime/include/syscall_trap.h deleted file mode 100644 index 33f525dd..00000000 --- a/runtime/include/syscall_trap.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -extern bool ltest_trap_syscall; - -namespace ltest { - -struct SyscallTrapGuard { - SyscallTrapGuard(); - ~SyscallTrapGuard(); -}; - -} // namespace ltest \ No newline at end of file diff --git a/runtime/include/verifying.h b/runtime/include/verifying.h index 848b9dfd..7aa8c301 100644 --- a/runtime/include/verifying.h +++ b/runtime/include/verifying.h @@ -14,7 +14,6 @@ #include "round_robin_strategy.h" #include "scheduler.h" #include "strategy_verifier.h" -#include "syscall_trap.h" #include "verifying_macro.h" namespace ltest { @@ -155,7 +154,6 @@ std::unique_ptr MakeScheduler(ModelChecker &checker, Opts &opts, inline int TrapRun(std::unique_ptr &&scheduler, PrettyPrinter &pretty_printer) { - auto guard = SyscallTrapGuard{}; auto result = scheduler->Run(); if (result.has_value()) { if (result->reason == Scheduler::NonLinearizableHistory::Reason::DEADLOCK) { diff --git a/runtime/include/yield_guard.h b/runtime/include/yield_guard.h deleted file mode 100644 index 1960c0bc..00000000 --- a/runtime/include/yield_guard.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -extern bool ltest_yield; - -namespace ltest { - -struct AllowYieldArea { - AllowYieldArea(); - ~AllowYieldArea(); -}; - -} // namespace ltest \ No newline at end of file diff --git a/runtime/lib.cpp b/runtime/lib.cpp index 3c6ce74c..e91e24de 100644 --- a/runtime/lib.cpp +++ b/runtime/lib.cpp @@ -4,9 +4,9 @@ #include #include +#include "coro_ctx_guard.h" #include "logger.h" #include "value_wrapper.h" -#include "yield_guard.h" // See comments in the lib.h. Task this_coro{}; @@ -14,8 +14,6 @@ Task this_coro{}; boost::context::fiber_context sched_ctx; std::optional coroutine_status; -BlockManager block_manager; - namespace ltest { std::vector task_builders{}; } @@ -32,7 +30,7 @@ void CoroBase::Resume() { // coroutine, area that protected by it should be as small as possible to // reduce errors { - ltest::AllowYieldArea guard{}; + ltest::CoroCtxGuard guard{}; boost::context::fiber_context([coro](boost::context::fiber_context&& ctx) { sched_ctx = std::move(ctx); coro->ctx = std::move(coro->ctx).resume(); @@ -61,7 +59,7 @@ std::string_view CoroBase::GetName() const { return name; } bool CoroBase::IsReturned() const { return is_returned; } extern "C" void CoroYield() { - if (!ltest_yield) [[unlikely]] { + if (!ltest_coro_ctx) [[unlikely]] { return; } assert(this_coro && sched_ctx); diff --git a/runtime/minimization.cpp b/runtime/minimization.cpp index d678ddb3..75f8ebc7 100644 --- a/runtime/minimization.cpp +++ b/runtime/minimization.cpp @@ -63,7 +63,8 @@ void GreedyRoundMinimizor::Minimize( if (new_histories.has_value()) { // sequential history (Invoke/Response events) could have odd number of - // history events in case if some task are not completed which is allowed by linearizability checker + // history events in case if some task are not completed which is + // allowed by linearizability checker nonlinear_history.full.swap(new_histories.value().full); nonlinear_history.seq.swap(new_histories.value().seq); diff --git a/runtime/syscall_trap.cpp b/runtime/syscall_trap.cpp deleted file mode 100644 index bde33561..00000000 --- a/runtime/syscall_trap.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "syscall_trap.h" - -/// Required for incapsulating syscall traps only in special places where it's -/// really needed -bool ltest_trap_syscall = 0; - -ltest::SyscallTrapGuard::SyscallTrapGuard() { ltest_trap_syscall = true; } - -ltest::SyscallTrapGuard::~SyscallTrapGuard() { ltest_trap_syscall = false; } diff --git a/runtime/yield_guard.cpp b/runtime/yield_guard.cpp deleted file mode 100644 index c4007ed6..00000000 --- a/runtime/yield_guard.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "yield_guard.h" - -/// Required for incapsulating CoroYield calls only in coroutines code, allowing -/// to call methods annotated with non_atomic in scheduler fiber -bool ltest_yield = 0; - -ltest::AllowYieldArea::AllowYieldArea() { ltest_yield = true; } - -ltest::AllowYieldArea::~AllowYieldArea() { ltest_yield = false; } diff --git a/syscall_intercept/hook.cpp b/syscall_intercept/hook.cpp index ffd64e62..681b1d4a 100644 --- a/syscall_intercept/hook.cpp +++ b/syscall_intercept/hook.cpp @@ -6,49 +6,53 @@ #include #include "runtime/include/block_manager.h" +#include "runtime/include/coro_ctx_guard.h" #include "runtime/include/lib.h" #include "runtime/include/logger.h" -#include "runtime/include/syscall_trap.h" -static int hook(long syscall_number, long arg0, long arg1, long arg2, long arg3, - long arg4, long arg5, long *result) { - if (!ltest_trap_syscall) { - return 1; - } - if (syscall_number == SYS_sched_yield) { - debug(stderr, "caught sched_yield()\n"); - CoroYield(); - *result = 0; - return 0; - } else if (syscall_number == SYS_futex) { - debug(stderr, "caught futex(0x%lx, %ld), exp: %ld, cur: %d\n", - (unsigned long)arg0, arg1, arg2, *((int *)arg0)); - arg1 = arg1 & FUTEX_CMD_MASK; - if (arg1 == FUTEX_WAIT || arg1 == FUTEX_WAIT_BITSET) { - auto fstate = BlockState{arg0, arg2}; - if (fstate.CanBeBlocked()) { - this_coro->SetBlocked(fstate); - CoroYield(); - *result = 0; - } else { - errno = EAGAIN; - *result = 1; - } - } else if (arg1 == FUTEX_WAKE || arg1 == FUTEX_WAKE_BITSET) { - debug(stderr, "caught wake\n"); - *result = block_manager.UnblockOn(arg0, arg2); +static int ltest_sched_yield(long *result) { + debug(stderr, "caught sched_yield()\n"); + CoroYield(); + *result = 0; + return 0; +} + +static int ltest_futex(long arg0, long arg1, long arg2, long *result) { + debug(stderr, "caught futex(0x%lx, %ld), exp: %ld, cur: %d\n", + (unsigned long)arg0, arg1, arg2, *((int *)arg0)); + arg1 = arg1 & FUTEX_CMD_MASK; + if (arg1 == FUTEX_WAIT || arg1 == FUTEX_WAIT_BITSET) { + auto fstate = BlockState{arg0, arg2}; + if (fstate.CanBeBlocked()) { + this_coro->SetBlocked(fstate); + CoroYield(); + *result = 0; } else { - assert(false && "unsupported futex call"); + errno = EAGAIN; + *result = 1; } - return 0; + } else if (arg1 == FUTEX_WAKE || arg1 == FUTEX_WAKE_BITSET) { + debug(stderr, "caught wake\n"); + *result = block_manager.UnblockOn(arg0, arg2); } else { - /* - * Ignore any other syscalls - * i.e.: pass them on to the kernel - * as would normally happen. - */ + assert(false && "unsupported futex call"); + } + return 0; +} + +static int hook(long syscall_number, long arg0, long arg1, long arg2, long arg3, + long arg4, long arg5, long *result) { + if (!ltest_coro_ctx) { return 1; } + switch (syscall_number) { + case SYS_sched_yield: + return ltest_sched_yield(result); + case SYS_futex: + return ltest_futex(arg0, arg1, arg2, result); + default: + return 1; + } } static __attribute__((constructor)) void init(void) { From fc150f5ee456d06c1902d680b7504237361a7655 Mon Sep 17 00:00:00 2001 From: Kirill Mitkin Date: Mon, 19 May 2025 22:27:48 +0000 Subject: [PATCH 3/5] make shared_mutex fair --- runtime/include/blocking_primitives.h | 65 +++++++++++++++++++---- runtime/include/pct_strategy.h | 75 ++++++++++++++------------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/runtime/include/blocking_primitives.h b/runtime/include/blocking_primitives.h index d439b848..fa21b3c8 100644 --- a/runtime/include/blocking_primitives.h +++ b/runtime/include/blocking_primitives.h @@ -38,7 +38,24 @@ struct mutex { friend struct condition_variable; }; -struct shared_mutex { +struct condition_variable { + as_atomic void wait(std::unique_lock& lock) { + addr = lock.mutex()->state.addr; + lock.unlock(); + this_coro->SetBlocked({addr, 1}); + CoroYield(); + lock.lock(); + } + + as_atomic void notify_one() { block_manager.UnblockOn(addr, 1); } + + as_atomic void notify_all() { block_manager.UnblockAllOn(addr); } + + private: + std::intptr_t addr; +}; + +struct shared_mutex_r { as_atomic void lock() { while (locked != 0) { this_coro->SetBlocked(state); @@ -67,21 +84,47 @@ struct shared_mutex { BlockState state{reinterpret_cast(&locked), locked}; }; -struct condition_variable { - as_atomic void wait(std::unique_lock& lock) { - addr = lock.mutex()->state.addr; - lock.unlock(); - this_coro->SetBlocked({addr, 1}); - CoroYield(); - lock.lock(); +struct shared_mutex { + as_atomic void lock() { + std::unique_lock lock{mutex_}; + while (write_) { + write_entered_.wait(lock); + } + write_ = true; + while (reader_count_ > 0) { + no_readers_.wait(lock); + } } - as_atomic void notify_one() { block_manager.UnblockOn(addr, 1); } + as_atomic void unlock() { + std::unique_lock lock{mutex_}; + write_ = false; + write_entered_.notify_all(); + } - as_atomic void notify_all() { block_manager.UnblockAllOn(addr); } + as_atomic void lock_shared() { + std::unique_lock lock{mutex_}; + while (write_) { + write_entered_.wait(lock); + } + ++reader_count_; + } + as_atomic void unlock_shared() { + std::unique_lock lock{mutex_}; + --reader_count_; + if (write_ && reader_count_ == 0) { + no_readers_.notify_one(); + } + } private: - std::intptr_t addr; + int reader_count_{0}; + bool write_{false}; + + ltest::condition_variable write_entered_; + ltest::condition_variable no_readers_; + + ltest::mutex mutex_; }; } // namespace ltest diff --git a/runtime/include/pct_strategy.h b/runtime/include/pct_strategy.h index a3dd8e82..02569b88 100644 --- a/runtime/include/pct_strategy.h +++ b/runtime/include/pct_strategy.h @@ -20,6 +20,37 @@ struct PctStrategy : public BaseStrategyWithThreads { PrepareForDepth(current_depth, 1); } + void AvoidLivelock(size_t& index, size_t& prior) { + auto& threads = this->threads; + if (fair_stage > 0) [[unlikely]] { + for (size_t attempt = 0; attempt < threads.size(); ++attempt) { + auto i = (++last_chosen) % threads.size(); + if (!threads[i].empty() && threads[i].back()->IsBlocked()) { + continue; + } + index = i; + prior = priorities[i]; + break; + } + // debug(stderr, "round robin choose: %d\n", index_of_max); + if (fair_start == index) { + --fair_stage; + } + } + + // TODO: Choose wiser constant + if (count_chosen_same == 1000 && index == last_chosen) [[unlikely]] { + fair_stage = 5; + fair_start = index; + } + + if (index == last_chosen) { + ++count_chosen_same; + } else { + count_chosen_same = 1; + } + } + std::optional NextThreadId() override { auto& threads = this->threads; size_t max = std::numeric_limits::min(); @@ -45,33 +76,7 @@ struct PctStrategy : public BaseStrategyWithThreads { } } - if (round_robin_stage > 0) [[unlikely]] { - for (size_t attempt = 0; attempt < threads.size(); ++attempt) { - auto i = (++last_chosen) % threads.size(); - if (!threads[i].empty() && threads[i].back()->IsBlocked()) { - continue; - } - index_of_max = i; - max = priorities[i]; - break; - } - // debug(stderr, "round robin choose: %d\n", index_of_max); - if (round_robin_start == index_of_max) { - --round_robin_stage; - } - } - - // TODO: Choose wiser constant - if (count_chosen_same == 1000 && index_of_max == last_chosen) [[unlikely]] { - round_robin_stage = 5; - round_robin_start = index_of_max; - } - - if (index_of_max == last_chosen) { - ++count_chosen_same; - } else { - count_chosen_same = 1; - } + AvoidLivelock(index_of_max, max); if (max == std::numeric_limits::min()) [[unlikely]] { return std::nullopt; @@ -117,7 +122,7 @@ struct PctStrategy : public BaseStrategyWithThreads { } } - if (round_robin_stage > 0) [[unlikely]] { + if (fair_stage > 0) [[unlikely]] { for (size_t attempt = 0; attempt < threads.size(); ++attempt) { auto i = (++last_chosen) % threads.size(); int task_index = this->GetNextTaskInThread(i); @@ -130,15 +135,15 @@ struct PctStrategy : public BaseStrategyWithThreads { break; } // debug(stderr, "round robin choose: %d\n", index_of_max); - if (round_robin_start == index_of_max) { - --round_robin_stage; + if (fair_start == index_of_max) { + --fair_stage; } } // TODO: Choose wiser constant if (count_chosen_same == 1000 && index_of_max == last_chosen) [[unlikely]] { - round_robin_stage = 5; - round_robin_start = index_of_max; + fair_stage = 5; + fair_start = index_of_max; } if (index_of_max == last_chosen) { @@ -194,7 +199,7 @@ struct PctStrategy : public BaseStrategyWithThreads { k_statistics.push_back(current_schedule_length); current_schedule_length = 0; count_chosen_same = 0; - round_robin_stage = 0; + fair_stage = 0; // current_depth have been increased size_t new_k = std::reduce(k_statistics.begin(), k_statistics.end()) / @@ -227,8 +232,8 @@ struct PctStrategy : public BaseStrategyWithThreads { // original article) size_t count_chosen_same; size_t last_chosen; - size_t round_robin_start; - size_t round_robin_stage{0}; + size_t fair_start; + size_t fair_stage{0}; std::vector priorities; std::vector priority_change_points; std::mt19937 rng; From d929fe66d5992cba5a091f74d90b9cdaf4bb2575 Mon Sep 17 00:00:00 2001 From: Kirill Mitkin Date: Thu, 29 May 2025 14:02:51 +0000 Subject: [PATCH 4/5] add nonlinear_buffered_channel --- runtime/include/verifying.h | 1 + verifying/blocking/CMakeLists.txt | 5 ++ verifying/blocking/buffered_channel.cpp | 60 +--------------- .../blocking/folly_flatcombining_queue.cpp | 3 +- .../blocking/nonlinear_buffered_channel.cpp | 69 +++++++++++++++++++ verifying/specs/buffered_channel.h | 60 ++++++++++++++++ 6 files changed, 140 insertions(+), 58 deletions(-) create mode 100644 verifying/blocking/nonlinear_buffered_channel.cpp create mode 100644 verifying/specs/buffered_channel.h diff --git a/runtime/include/verifying.h b/runtime/include/verifying.h index 7aa8c301..10a9c3d4 100644 --- a/runtime/include/verifying.h +++ b/runtime/include/verifying.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include diff --git a/verifying/blocking/CMakeLists.txt b/verifying/blocking/CMakeLists.txt index aa32b7af..0aec8d23 100644 --- a/verifying/blocking/CMakeLists.txt +++ b/verifying/blocking/CMakeLists.txt @@ -5,6 +5,7 @@ set (SOURCE_TARGET_LIST shared_mutexed_register.cpp bank.cpp buffered_channel.cpp + nonlinear_buffered_channel.cpp simple_deadlock.cpp bank_deadlock.cpp ) @@ -79,6 +80,10 @@ add_integration_test_blocking("buffered_channel_pct" "verify" FALSE buffered_channel --strategy pct --rounds 10000 ) +add_integration_test_blocking("nonlinear_buffered_channel_pct" "verify" TRUE + nonlinear_buffered_channel --strategy pct --rounds 10000 +) + add_integration_test_blocking("buffered_channel_random" "verify" FALSE buffered_channel --strategy random --rounds 10000 ) diff --git a/verifying/blocking/buffered_channel.cpp b/verifying/blocking/buffered_channel.cpp index c8cc3b1b..b303813f 100644 --- a/verifying/blocking/buffered_channel.cpp +++ b/verifying/blocking/buffered_channel.cpp @@ -1,67 +1,13 @@ +#include "../specs/buffered_channel.h" + #include #include #include #include -#include "../specs/queue.h" -#include "runtime/include/value_wrapper.h" #include "runtime/include/verifying.h" #include "verifiers/buffered_channel_verifier.h" -constexpr int N = 5; - -namespace spec { -struct BufferedChannel { - void Send(int v) { deq.push_back(v); } - - int Recv() { - if (deq.empty()) { - return -1; - } - auto value = deq.front(); - deq.pop_front(); - return value; - } - - using method_t = std::function; - static auto GetMethods() { - method_t send_func = [](BufferedChannel *l, void *args) { - auto real_args = reinterpret_cast *>(args); - l->Send(std::get<0>(*real_args)); - return void_v; - }; - - method_t recv_func = [](BufferedChannel *l, void *args) -> int { - return l->Recv(); - }; - - return std::map{ - {"Send", send_func}, - {"Recv", recv_func}, - }; - } - - std::deque deq; -}; - -struct BufferedChannelHash { - size_t operator()(const BufferedChannel &r) const { - int res = 0; - for (int elem : r.deq) { - res += elem; - } - return res; - } -}; - -struct BufferedChannelEquals { - bool operator()(const BufferedChannel &lhs, - const BufferedChannel &rhs) const { - return lhs.deq == rhs.deq; - } -}; -}; // namespace spec - struct BufferedChannel { non_atomic void Send(int v) { std::unique_lock lock{mutex_}; @@ -86,7 +32,7 @@ struct BufferedChannel { } debug(stderr, "Recv\n"); auto val = queue_[ridx_]; - ridx_ = (ridx_ + 1) % 5; + ridx_ = (ridx_ + 1) % N; empty_ = (sidx_ == ridx_); full_ = false; send_side_cv_.notify_one(); diff --git a/verifying/blocking/folly_flatcombining_queue.cpp b/verifying/blocking/folly_flatcombining_queue.cpp index 6a1075c1..8be1a3f1 100644 --- a/verifying/blocking/folly_flatcombining_queue.cpp +++ b/verifying/blocking/folly_flatcombining_queue.cpp @@ -5,7 +5,8 @@ #include "runtime/include/verifying_macro.h" #include "verifying/specs/queue.h" -class FlatCombiningQueue : public folly::FlatCombining { +class FlatCombiningQueue + : public folly::FlatCombining { spec::Queue<> queue_; public: diff --git a/verifying/blocking/nonlinear_buffered_channel.cpp b/verifying/blocking/nonlinear_buffered_channel.cpp new file mode 100644 index 00000000..f6d884b9 --- /dev/null +++ b/verifying/blocking/nonlinear_buffered_channel.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include + +#include "../specs/buffered_channel.h" +#include "runtime/include/verifying.h" +#include "verifiers/buffered_channel_verifier.h" + +struct BufferedChannel { + non_atomic void Send(int v) { + std::unique_lock lock{mutex_}; + while (!closed_ && full_) { + debug(stderr, "Waiting in send...\n"); + send_side_cv_.wait(lock); + } + debug(stderr, "Send\n"); + + queue_[sidx_] = v; + sidx_ = (sidx_ + 1) % N; + empty_ = false; + recv_side_cv_.notify_one(); + } + + non_atomic int Recv() { + std::unique_lock lock{mutex_}; + while (!closed_ && empty_) { + debug(stderr, "Waiting in recv...\n"); + recv_side_cv_.wait(lock); + } + debug(stderr, "Recv\n"); + auto val = queue_[ridx_]; + ridx_ = (ridx_ + 1) % N; + empty_ = (sidx_ == ridx_); + full_ = false; + send_side_cv_.notify_one(); + return val; + } + + void Close() { + closed_.store(true); + send_side_cv_.notify_all(); + recv_side_cv_.notify_all(); + } + + std::mutex mutex_; + std::condition_variable send_side_cv_, recv_side_cv_; + std::atomic closed_{false}; + + bool full_{false}; + bool empty_{true}; + + uint32_t sidx_{0}, ridx_{0}; + + std::array queue_{}; +}; + +auto generateInt(size_t) { + return ltest::generators::makeSingleArg(rand() % 10 + 1); +} + +using spec_t = + ltest::Spec; + +LTEST_ENTRYPOINT_CONSTRAINT(spec_t, spec::BufferedChannelVerifier); + +target_method(generateInt, void, BufferedChannel, Send, int); +target_method(ltest::generators::genEmpty, int, BufferedChannel, Recv); diff --git a/verifying/specs/buffered_channel.h b/verifying/specs/buffered_channel.h new file mode 100644 index 00000000..80cdc8bb --- /dev/null +++ b/verifying/specs/buffered_channel.h @@ -0,0 +1,60 @@ + +#include +#include +#include + +#include "runtime/include/value_wrapper.h" + +constexpr int N = 5; + +namespace spec { +struct BufferedChannel { + void Send(int v) { deq.push_back(v); } + + int Recv() { + if (deq.empty()) { + return -1; + } + auto value = deq.front(); + deq.pop_front(); + return value; + } + + using method_t = std::function; + static auto GetMethods() { + method_t send_func = [](BufferedChannel *l, void *args) { + auto real_args = reinterpret_cast *>(args); + l->Send(std::get<0>(*real_args)); + return void_v; + }; + + method_t recv_func = [](BufferedChannel *l, void *args) -> int { + return l->Recv(); + }; + + return std::map{ + {"Send", send_func}, + {"Recv", recv_func}, + }; + } + + std::deque deq; +}; + +struct BufferedChannelHash { + size_t operator()(const BufferedChannel &r) const { + int res = 0; + for (int elem : r.deq) { + res += elem; + } + return res; + } +}; + +struct BufferedChannelEquals { + bool operator()(const BufferedChannel &lhs, + const BufferedChannel &rhs) const { + return lhs.deq == rhs.deq; + } +}; +}; // namespace spec \ No newline at end of file From 76497e2ed90a95e65755a110c85aab9a4719fb31 Mon Sep 17 00:00:00 2001 From: Kirill Mitkin Date: Sat, 31 May 2025 11:34:55 +0000 Subject: [PATCH 5/5] rise up timeout --- .github/workflows/run-tests.yaml | 2 +- CMakeLists.txt | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 3b561e38..cd39b5d0 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -31,7 +31,7 @@ jobs: container: image: silkeh/clang:19 options: --user root - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Install deps run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 8234d0eb..fe508ba5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,13 +9,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(APPLY_CLANG_TOOL ON) -# TODO(kmitkin): require to understand, what is it considered to be "optimized" build -# set(CMAKE_CXX_FLAGS_RELEASE "???") -# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - set(CMAKE_CONFIGURATION_TYPES "Debug;Release;RelWithAssert" CACHE STRING "" FORCE) - set(CMAKE_C_FLAGS_RELWITHASSERT "${CMAKE_C_FLAGS_RELEASE} -UNDEBUG" CACHE STRING "" FORCE) set(CMAKE_CXX_FLAGS_RELWITHASSERT "${CMAKE_CXX_FLAGS_RELEASE} -UNDEBUG" CACHE STRING "" FORCE)