From effa4e23a4a5fca821552794b427cb30899d3a71 Mon Sep 17 00:00:00 2001 From: Martin Helmut Fieber Date: Sat, 2 Jul 2022 21:39:18 +0200 Subject: [PATCH] Basic project setup --- .clang-format | 16 ++ .clang-tidy | 40 ++++ .editorconfig | 9 + .gitignore | 4 + CMakeLists.txt | 21 ++ CODE_OF_CONDUCT.md | 123 +++++++++++ LICENSE | 21 ++ README.md | 13 ++ cmake/CompilerWarnings.cmake | 89 ++++++++ cmake/StandardProjectSettings.cmake | 29 +++ cmake/StaticAnalyzers.cmake | 12 + cmake/UniversalAppleBuild.cmake | 5 + litr.toml | 29 +++ src/CMakeLists.txt | 2 + src/app/App/Main.cpp | 23 ++ src/app/CMakeLists.txt | 9 + src/some_library/CMakeLists.txt | 11 + src/some_library/SomeLibrary/Core/Log.cpp | 32 +++ src/some_library/SomeLibrary/Core/Log.hpp | 66 ++++++ .../SomeLibrary/Debug/Instrumentor.hpp | 209 ++++++++++++++++++ vendor/CMakeLists.txt | 22 ++ vendor/doctest/CMakeLists.txt | 3 + vendor/fmt/CMakeLists.txt | 3 + vendor/spdlog/CMakeLists.txt | 5 + 24 files changed, 796 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/CompilerWarnings.cmake create mode 100644 cmake/StandardProjectSettings.cmake create mode 100644 cmake/StaticAnalyzers.cmake create mode 100644 cmake/UniversalAppleBuild.cmake create mode 100644 litr.toml create mode 100644 src/CMakeLists.txt create mode 100644 src/app/App/Main.cpp create mode 100644 src/app/CMakeLists.txt create mode 100644 src/some_library/CMakeLists.txt create mode 100644 src/some_library/SomeLibrary/Core/Log.cpp create mode 100644 src/some_library/SomeLibrary/Core/Log.hpp create mode 100644 src/some_library/SomeLibrary/Debug/Instrumentor.hpp create mode 100644 vendor/CMakeLists.txt create mode 100644 vendor/doctest/CMakeLists.txt create mode 100644 vendor/fmt/CMakeLists.txt create mode 100644 vendor/spdlog/CMakeLists.txt diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..dd02aae --- /dev/null +++ b/.clang-format @@ -0,0 +1,16 @@ +--- +Language: Cpp +BasedOnStyle: Google +AlignAfterOpenBracket: DontAlign +AllowShortBlocksOnASingleLine: Empty +AllowShortFunctionsOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Empty +AllowShortLoopsOnASingleLine: false +AllowAllConstructorInitializersOnNextLine: false +BinPackArguments: false +BinPackParameters: false +ColumnLimit: 100 + +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..fd044e2 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,40 @@ +--- +Checks: > + *, + -android-*, + -abseil-*, + -altera-*, + -darwin-*, + -fuchsia-*, + -google-*, + -objc-*, + -zircon-*, + -llvm*, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-macro-usage, + -readability-function-cognitive-complexity, + -misc-non-private-member-variables-in-classes, + -misc-static-assert, + -modernize-use-trailing-return-type, + -bugprone-easily-swappable-parameters, + -cert-env33-c, + -cert-err58-cpp + +WarningsAsErrors: '*' +HeaderFilterRegex: '' +FormatStyle: none + +CheckOptions: + - { key: readability-identifier-naming.NamespaceCase, value: CamelCase } + - { key: readability-identifier-naming.ClassCase, value: CamelCase } + - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } + - { key: readability-identifier-naming.StructCase, value: CamelCase } + - { key: readability-identifier-naming.ClassMethodCase, value: lower_case } + - { key: readability-identifier-naming.ClassMemberCase, value: lower_case } + - { key: readability-identifier-naming.FunctionCase, value: lower_case } + - { key: readability-identifier-naming.VariableCase, value: lower_case } + - { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE } + - { key: readability-identifier-length.MinimumVariableNameLength, value: 2 } + - { key: readability-identifier-length.MinimumParameterNameLength, value: 2 } + - { key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors, value: '1' } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f17867 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecd7d49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +profile.json +*-profile.json +*.log diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b2e4044 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.22) + +include(cmake/UniversalAppleBuild.cmake) + +project( + BasicProjectSetup + DESCRIPTION "Base project setup." + LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(cmake/StandardProjectSettings.cmake) + +# Link this "library" to use the warnings specified in CompilerWarnings.cmake +add_library(project_warnings INTERFACE) +include(cmake/CompilerWarnings.cmake) +set_project_warnings(project_warnings) + +add_subdirectory(vendor) +add_subdirectory(src) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9dcc93c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,123 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[info@martin-fieber.se](mailto:info@martin-fieber.se). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f41d1f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Martin Helmut Fieber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0963f0f --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Base project setup + +C++ project template. + +## Setup + +This project requires [Litr](https://github.com/krieselreihe/litr). + +Quick setup to build and run the application: + +```shell +litr build,start +``` diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake new file mode 100644 index 0000000..35d3c85 --- /dev/null +++ b/cmake/CompilerWarnings.cmake @@ -0,0 +1,89 @@ +# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md + +function(set_project_warnings project_name) + option(WARNINGS_AS_ERRORS "Treat compiler warnings as errors" TRUE) + message(STATUS "Treat compiler warnings as errors") + + set(MSVC_WARNINGS + /W4 # Baseline reasonable warnings + /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss + # of data + /w14254 # 'operator': conversion from 'type1:field_bits' to + # 'type2:field_bits', possible loss of data + /w14263 # 'function': member function does not override any base class + # virtual member function + /w14265 # 'classname': class has virtual functions, but destructor is not + # virtual instances of this class may not be destructed correctly + /w14287 # 'operator': unsigned/negative constant mismatch + /we4289 # nonstandard extension used: 'variable': loop control variable + # declared in the for-loop is used outside the for-loop scope + /w14296 # 'operator': expression is always 'boolean_value' + /w14311 # 'variable': pointer truncation from 'type1' to 'type2' + /w14545 # expression before comma evaluates to a function which is missing + # an argument list + /w14546 # function call before comma missing argument list + /w14547 # 'operator': operator before comma has no effect; expected + # operator with side-effect + /w14549 # 'operator': operator before comma has no effect; did you intend + # 'operator'? + /w14555 # expression has no effect; expected expression with side- effect + /w14619 # pragma warning: there is no warning number 'number' + /w14640 # Enable warning on thread un-safe static member initialization + /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may + # cause unexpected runtime behavior. + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + /w14928 # illegal copy-initialization; more than one user-defined + # conversion has been implicitly applied + /permissive- # standards conformance mode for MSVC compiler. + ) + + set(CLANG_WARNINGS + -Wall + -Wextra # reasonable and standard + # -Wshadow # warn the user if a variable declaration shadows one from a + # parent context + -Wnon-virtual-dtor # warn the user if a class with virtual functions has a + # non-virtual destructor. This helps catch hard to + # track down memory errors + -Wold-style-cast # warn for c-style casts + -Wcast-align # warn for potential performance problem casts + -Wunused # warn on anything being unused + -Woverloaded-virtual # warn if you overload (not override) a virtual + # function + -Wpedantic # warn if non-standard C++ is used + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + -Wformat=2 # warn on security issues around functions that format output + # (ie printf) + ) + + if (WARNINGS_AS_ERRORS) + set(CLANG_WARNINGS ${CLANG_WARNINGS} -Werror) + set(MSVC_WARNINGS ${MSVC_WARNINGS} /WX) + endif () + + set(GCC_WARNINGS + ${CLANG_WARNINGS} + -Wmisleading-indentation # warn if indentation implies blocks where blocks + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wlogical-op # warn about logical operations being used where bitwise were + -Wuseless-cast # warn if you perform a cast to the same type + ) + + if (MSVC) + set(PROJECT_WARNINGS ${MSVC_WARNINGS}) + elseif (CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(PROJECT_WARNINGS ${CLANG_WARNINGS}) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(PROJECT_WARNINGS ${GCC_WARNINGS}) + else () + message(AUTHOR_WARNING "No compiler warnings set for '${CMAKE_CXX_COMPILER_ID}' compiler.") + endif () + + target_compile_options(${project_name} INTERFACE ${PROJECT_WARNINGS}) + +endfunction() diff --git a/cmake/StandardProjectSettings.cmake b/cmake/StandardProjectSettings.cmake new file mode 100644 index 0000000..da64900 --- /dev/null +++ b/cmake/StandardProjectSettings.cmake @@ -0,0 +1,29 @@ +# Set a default build type if none was specified +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Debug' as none was specified.") + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE) + + # Set the possible values of build type for cmake-gui, ccmake + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release") +endif () + +find_program(CCACHE ccache) +if (CCACHE) + message(STATUS "Using ccache") + set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE}) +else () + message(STATUS "Ccache not found") +endif () + +# Generate compile_commands.json to make it easier to work with clang based tools +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +option(DEACTIVATE_LOGGING "Disable logging" OFF) +if (DEACTIVATE_LOGGING) + add_compile_definitions(APP_DEACTIVATE_LOGGING) +endif () + +option(DEBUG "Enable debug statements and asserts" OFF) +if (DEBUG OR CMAKE_BUILD_TYPE STREQUAL "Debug") + add_compile_definitions(DEBUG APP_ENABLE_ASSERTS APP_PROFILE) +endif () diff --git a/cmake/StaticAnalyzers.cmake b/cmake/StaticAnalyzers.cmake new file mode 100644 index 0000000..bc36144 --- /dev/null +++ b/cmake/StaticAnalyzers.cmake @@ -0,0 +1,12 @@ +if (NOT RELEASE) + find_program(CLANGTIDY clang-tidy) + if (CLANGTIDY) + message(STATUS "Using clang-tidy") + set(CMAKE_CXX_CLANG_TIDY ${CLANGTIDY}) + else () + message(SEND_ERROR "clang-tidy requested but executable not found") + endif () + + message(STATUS "Using address sanitizer") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -fsanitize=address -g") +endif () diff --git a/cmake/UniversalAppleBuild.cmake b/cmake/UniversalAppleBuild.cmake new file mode 100644 index 0000000..6abd2d7 --- /dev/null +++ b/cmake/UniversalAppleBuild.cmake @@ -0,0 +1,5 @@ +# Generate universal executable for Apple hardware +# This file needs to be included before calling `project`. +if (APPLE AND RELEASE) + set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "") +endif () diff --git a/litr.toml b/litr.toml new file mode 100644 index 0000000..0f7f892 --- /dev/null +++ b/litr.toml @@ -0,0 +1,29 @@ +[commands.build] +script = [ + """cmake -GNinja %{debug '-DDEBUG=ON'} -DCMAKE_BUILD_TYPE=%{target} -B build/%{target}""", + "ninja -C build/%{target}" +] +description = "Build the application for a given target." + +[commands.start] +script = "./build/%{target}/src/app/App" +description = "Start the application." + +[commands.test] +script = "echo 'No tests defined, yet!'" +description = "Run all tests." + +[commands.format] +script = "find src -iname *.hpp -o -iname *.cpp | xargs clang-format -i" +description = "Format project sources via clang-format." + +[params.target] +shortcut = "t" +description = "Define the application build target." +type = ["debug", "release"] +default = "debug" + +[params.debug] +shortcut = "d" +description = "Set debug mode, even if build type differs." +type = "boolean" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..5f86116 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(app) +add_subdirectory(some_library) diff --git a/src/app/App/Main.cpp b/src/app/App/Main.cpp new file mode 100644 index 0000000..f97ed2c --- /dev/null +++ b/src/app/App/Main.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022 Martin Helmut Fieber + */ + +#include "SomeLibrary/Core/Log.hpp" +#include "SomeLibrary/Debug/Instrumentor.hpp" + +int main() { + try { + APP_PROFILE_BEGIN_SESSION_WITH_FILE("App", "profile.json"); + + { + APP_PROFILE_SCOPE("Test scope"); + APP_INFO("Hello World\n"); + } + + APP_PROFILE_END_SESSION(); + } catch (std::exception& e) { + APP_ERROR("Main process terminated with: {}", e.what()); + } + + return 0; +} diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 0000000..5afa4b2 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,9 @@ +set(NAME "App") + +include(${PROJECT_SOURCE_DIR}/cmake/StaticAnalyzers.cmake) + +add_executable(${NAME} App/Main.cpp) + +target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(${NAME} PRIVATE cxx_std_20) +target_link_libraries(${NAME} PRIVATE project_warnings SomeLibrary) diff --git a/src/some_library/CMakeLists.txt b/src/some_library/CMakeLists.txt new file mode 100644 index 0000000..02ee77e --- /dev/null +++ b/src/some_library/CMakeLists.txt @@ -0,0 +1,11 @@ +set(NAME "SomeLibrary") + +include(${PROJECT_SOURCE_DIR}/cmake/StaticAnalyzers.cmake) + +add_library(${NAME} STATIC + SomeLibrary/Core/Log.cpp SomeLibrary/Core/Log.hpp + SomeLibrary/Debug/Instrumentor.hpp) + +target_include_directories(${NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_features(${NAME} PRIVATE cxx_std_20) +target_link_libraries(${NAME} PRIVATE project_warnings PUBLIC fmt spdlog) diff --git a/src/some_library/SomeLibrary/Core/Log.cpp b/src/some_library/SomeLibrary/Core/Log.cpp new file mode 100644 index 0000000..8c059aa --- /dev/null +++ b/src/some_library/SomeLibrary/Core/Log.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 Martin Helmut Fieber + */ + +#include "Log.hpp" + +#include + +#include "spdlog/sinks/basic_file_sink.h" +#include "spdlog/sinks/stdout_color_sinks.h" + +namespace App { + +Log::Log() { + std::vector log_sinks; + + spdlog::level::level_enum level{spdlog::level::debug}; + + log_sinks.emplace_back(std::make_shared()); + log_sinks.emplace_back(std::make_shared("app.log", true)); + + log_sinks[0]->set_pattern("%^[%T] %n(%l): %v%$"); + log_sinks[1]->set_pattern("[%T] [%l] %n(%l): %v"); + + m_logger = std::make_shared("APP", begin(log_sinks), end(log_sinks)); + spdlog::register_logger(m_logger); + spdlog::set_default_logger(m_logger); + m_logger->set_level(level); + m_logger->flush_on(level); +} + +} // namespace App diff --git a/src/some_library/SomeLibrary/Core/Log.hpp b/src/some_library/SomeLibrary/Core/Log.hpp new file mode 100644 index 0000000..a11120d --- /dev/null +++ b/src/some_library/SomeLibrary/Core/Log.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Martin Helmut Fieber + */ + +#pragma once + +#include + +#include "spdlog/fmt/ostr.h" +#include "spdlog/spdlog.h" + +namespace App { + +class Log { + public: + Log(const Log&) = delete; + Log(const Log&&) = delete; + Log& operator=(const Log&) = delete; + Log& operator=(const Log&&) = delete; + ~Log() = default; + + static std::shared_ptr& logger() { + return get().m_logger; + } + + private: + // The constructor shall not be deleted but used to bootstrap the logger. Ignoring + // the lint warning is ignoring doing `Log() = delete`. + // NOLINTNEXTLINE + Log(); + + static Log& get() { + static Log instance{}; + return instance; + } + + std::shared_ptr m_logger; +}; + +} // namespace App + +#ifndef APP_DEACTIVATE_LOGGING + +#if DEBUG +#define APP_TRACE(...) ::App::Log::logger()->trace(__VA_ARGS__) +#define APP_DEBUG(...) ::App::Log::logger()->debug(__VA_ARGS__) +#else +#define APP_TRACE(...) +#define APP_DEBUG(...) +#endif + +#define APP_INFO(...) ::App::Log::logger()->info(__VA_ARGS__) +#define APP_WARN(...) ::App::Log::logger()->warn(__VA_ARGS__) +#define APP_ERROR(...) ::App::Log::logger()->error(__VA_ARGS__) +#define APP_FATAL(...) ::App::Log::logger()->fatal(__VA_ARGS__) + +#else + +#define APP_TRACE(...) +#define APP_DEBUG(...) +#define APP_INFO(...) +#define APP_WARN(...) +#define APP_ERROR(...) +#define APP_FATAL(...) + +#endif diff --git a/src/some_library/SomeLibrary/Debug/Instrumentor.hpp b/src/some_library/SomeLibrary/Debug/Instrumentor.hpp new file mode 100644 index 0000000..acaf957 --- /dev/null +++ b/src/some_library/SomeLibrary/Debug/Instrumentor.hpp @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2022 Martin Helmut Fieber + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SomeLibrary/Core/Log.hpp" + +namespace App::Debug { + +using FloatingPointMicroseconds = std::chrono::duration; + +struct ProfileResult { + std::string name; + FloatingPointMicroseconds start; + std::chrono::microseconds elapsed_time; + std::thread::id thread_id; +}; + +struct InstrumentationSession { + const std::string name; + explicit InstrumentationSession(std::string name) : name(std::move(name)) {} +}; + +class Instrumentor { + public: + Instrumentor(const Instrumentor&) = delete; + Instrumentor(Instrumentor&&) = delete; + Instrumentor& operator=(Instrumentor other) = delete; + Instrumentor& operator=(Instrumentor&& other) = delete; + + void begin_session(const std::string& name, const std::string& filepath = "results.json") { + std::lock_guard lock(m_mutex); + + if (m_current_session != nullptr) { + // If there is already a current session, then close it before beginning new one. + // Subsequent profiling output meant for the original session will end up in the + // newly opened session instead. That's better than having badly formatted + // profiling output. + APP_ERROR("Instrumentor::begin_session('{0}') when session '{1}' already open.", + name, + m_current_session->name); + internal_end_session(); + } + m_output_stream.open(filepath); + + if (m_output_stream.is_open()) { + m_current_session = std::make_unique(name); + write_header(); + } else { + APP_ERROR("Instrumentor could not open results file '{0}'.", filepath); + } + } + + void end_session() { + std::lock_guard lock(m_mutex); + internal_end_session(); + } + + void write_profile(const ProfileResult& result) { + std::stringstream json; + + std::string name{result.name}; + std::replace(name.begin(), name.end(), '"', '\''); + + json << std::setprecision(3) << std::fixed; + json << ",{"; + json << R"("cat":"function",)"; + json << "\"dur\":" << (result.elapsed_time.count()) << ','; + json << R"("name":")" << name << "\","; + json << R"("ph":"X",)"; + json << "\"pid\":0,"; + json << R"("tid":")" << result.thread_id << "\","; + json << "\"ts\":" << result.start.count(); + json << "}"; + + std::lock_guard lock(m_mutex); + if (m_current_session != nullptr) { + m_output_stream << json.str(); + m_output_stream.flush(); + } + } + + static Instrumentor& get() { + static Instrumentor instance; + return instance; + } + + private: + Instrumentor() : m_current_session(nullptr) {} + + ~Instrumentor() { + end_session(); + } + + void write_header() { + m_output_stream << R"({"otherData": {},"traceEvents":[{})"; + m_output_stream.flush(); + } + + void write_footer() { + m_output_stream << "]}"; + m_output_stream.flush(); + } + + // Note: you must already own lock on m_Mutex before + // calling InternalEndSession() + void internal_end_session() { + if (m_current_session != nullptr) { + write_footer(); + m_output_stream.close(); + } + } + + std::mutex m_mutex; + std::unique_ptr m_current_session; + std::ofstream m_output_stream; +}; + +class InstrumentationTimer { + public: + explicit InstrumentationTimer(std::string name) + : m_name(std::move(name)), + m_start_time_point(std::chrono::steady_clock::now()) {} + + InstrumentationTimer(const InstrumentationTimer&) = delete; + InstrumentationTimer(InstrumentationTimer&&) = delete; + InstrumentationTimer& operator=(InstrumentationTimer other) = delete; + InstrumentationTimer& operator=(InstrumentationTimer&& other) = delete; + + ~InstrumentationTimer() { + if (!m_stopped) { + stop(); + } + } + + void stop() { + const auto end_time_point{std::chrono::steady_clock::now()}; + const auto high_res_start{FloatingPointMicroseconds{m_start_time_point.time_since_epoch()}}; + const auto elapsed_time{ + std::chrono::time_point_cast(end_time_point).time_since_epoch() - + std::chrono::time_point_cast(m_start_time_point) + .time_since_epoch()}; + + Instrumentor::get().write_profile( + {m_name, high_res_start, elapsed_time, std::this_thread::get_id()}); + + m_stopped = true; + } + + private: + const std::string m_name; + bool m_stopped{false}; + const std::chrono::time_point m_start_time_point; +}; + +} // namespace App::Debug + +#if APP_PROFILE +// Resolve which function signature macro will be used. Note that this only +// is resolved when the (pre)compiler starts, so the syntax highlighting +// could mark the wrong one in your editor! +#if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || \ + (defined(__ICC) && (__ICC >= 600)) || defined(__ghs__) +// There is an implicit decay of an array into a pointer. +// NOLINTNEXTLINE +#define APP_FUNC_SIG __PRETTY_FUNCTION__ +#elif defined(__DMC__) && (__DMC__ >= 0x810) +#define APP_FUNC_SIG __PRETTY_FUNCTION__ +#elif defined(__FUNCSIG__) +#define APP_FUNC_SIG __FUNCSIG__ +#elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || \ + (defined(__IBMCPP__) && (__IBMCPP__ >= 500)) +#define APP_FUNC_SIG __FUNCTION__ +#elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550) +#define APP_FUNC_SIG __FUNC__ +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901) +#define APP_FUNC_SIG __func__ +#elif defined(__cplusplus) && (__cplusplus >= 201103) +#define APP_FUNC_SIG __func__ +#else +#define APP_FUNC_SIG "APP_FUNC_SIG unknown!" +#endif + +#define JOIN_AGAIN(x, y) x##y +#define JOIN(x, y) JOIN_AGAIN(x, y) +#define APP_PROFILE_BEGIN_SESSION(name) ::App::Debug::Instrumentor::get().begin_session(name) +#define APP_PROFILE_BEGIN_SESSION_WITH_FILE(name, file_path) \ + ::App::Debug::Instrumentor::get().begin_session(name, file_path) +#define APP_PROFILE_END_SESSION() ::App::Debug::Instrumentor::get().end_session() +#define APP_PROFILE_SCOPE(name) \ + ::App::Debug::InstrumentationTimer JOIN(timer, __LINE__) { name } +#define APP_PROFILE_FUNCTION() APP_PROFILE_SCOPE(APP_FUNC_SIG) +#else +#define APP_PROFILE_BEGIN_SESSION(name) +#define APP_PROFILE_BEGIN_SESSION_WITH_FILE(name, file_path) +#define APP_PROFILE_END_SESSION() +#define APP_PROFILE_SCOPE(name) +#define APP_PROFILE_FUNCTION() +#endif diff --git a/vendor/CMakeLists.txt b/vendor/CMakeLists.txt new file mode 100644 index 0000000..88a3373 --- /dev/null +++ b/vendor/CMakeLists.txt @@ -0,0 +1,22 @@ +include(FetchContent) + +FetchContent_Declare( + doctest + GIT_REPOSITORY "https://github.com/onqtam/doctest.git" + GIT_TAG 7b9885133108ae301ddd16e2651320f54cafeba7 # 2.4.8 +) +add_subdirectory(doctest) + +FetchContent_Declare( + fmt + GIT_REPOSITORY "https://github.com/fmtlib/fmt.git" + GIT_TAG b6f4ceaed0a0a24ccf575fab6c56dd50ccf6f1a9 # 8.1.1 +) +add_subdirectory(fmt) + +FetchContent_Declare( + spdlog + GIT_REPOSITORY "https://github.com/gabime/spdlog.git" + GIT_TAG 76fb40d95455f249bd70824ecfcae7a8f0930fa3 # 1.10.0 +) +add_subdirectory(spdlog) diff --git a/vendor/doctest/CMakeLists.txt b/vendor/doctest/CMakeLists.txt new file mode 100644 index 0000000..2afc2a6 --- /dev/null +++ b/vendor/doctest/CMakeLists.txt @@ -0,0 +1,3 @@ +message(STATUS "Fetching Doctest ...") + +FetchContent_MakeAvailable(doctest) diff --git a/vendor/fmt/CMakeLists.txt b/vendor/fmt/CMakeLists.txt new file mode 100644 index 0000000..9fffb7b --- /dev/null +++ b/vendor/fmt/CMakeLists.txt @@ -0,0 +1,3 @@ +message(STATUS "Fetching fmt ...") + +FetchContent_MakeAvailable(fmt) diff --git a/vendor/spdlog/CMakeLists.txt b/vendor/spdlog/CMakeLists.txt new file mode 100644 index 0000000..29bc932 --- /dev/null +++ b/vendor/spdlog/CMakeLists.txt @@ -0,0 +1,5 @@ +message(STATUS "Fetching spdlog ...") + +set(SPDLOG_FMT_EXTERNAL "ON") + +FetchContent_MakeAvailable(spdlog)