Contributing to timemory

This document will primarily cover introducing new components to timemory, however other pull requests for bug-fixes and such are encouraged. The design of timemory enables there to be two categories for introducing new components to timemory:

  1. Components whose code will reside in the timemory source code

  2. Components whose definitions will reside in an external library

Either #1 or #2 is acceptable. In general, if the component(s) provide an interface to another tool, it is preferred that you just make the necessary additions to the build system to find the package, creates a types.hpp header which declares the component types and provides some metadata for the timemory-avail tool, and a extern.hpp + extern.cpp files for instantiating some of the templates when your tool is enabled. Beyond this, every thing else can be freely modified in your source code. See the Version and Interface Compatibility section for long-term compatibility concerns.

Basic Layout

  • source/timemory/components/hello_world/

  • source/timemory/components/hello_world/CMakeLists.txt

  • source/timemory/components/hello_world/types.hpp

  • source/timemory/components/hello_world/backends.hpp (optional)

  • source/timemory/components/hello_world/extern.hpp (optional, recommended)

  • source/timemory/components/hello_world/extern.cpp (optional, recommended)

  • source/timemory/components/hello_world/components.hpp (optional, recommended)

See also: source/timemory/components/README.md

Step 1. Create source/timemory/components/hello_world/ folder

Folder containing all the necessary files for the component

Step 2. source/timemory/components/hello_world/CMakeLists.txt

  • Add an option: add_option(TIMEMORY_USE_HELLO_WORLD "Enable HELLO_WORLD support" OFF)

  • Add an interface library: add_interface_library(timemory-hello-world "Enables HELLO_WORLD support")

    • Use this library to set all the build-flags, include paths, etc.

  • Link the interface library to the timemory-extensions target

  • Use the build_intermediate_library(...) to build the extern templates

  • Include this directory in source/timemory/components/CMakeLists.txt

# use ON if you want the package to be picked up in auto-detect mode
define_default_option(_HELLO_WORLD ON)
add_option(TIMEMORY_USE_HELLO_WORLD "Enable HELLO_WORLD support" ${_HELLO_WORLD})
add_interface_library(timemory-hello-world "Enables HELLOW_WORLD support")

if(TIMEMORY_USE_HELLO_WORLD)
    # if TIMEMORY_REQUIRE_PACKAGES=ON and user requested HELLO_WORLD, this will be fatal
    find_package(HelloWorld ${TIMEMORY_FIND_REQUIREMENT})
endif()

# this will only be entered if auto-detect mode is enabled, use FORCE so
# that this is correctly displayed in final report
if(NOT HelloWorld_FOUND)
    set(TIMEMORY_USE_HELLO_WORLD OFF CACHE BOOL "Hello World" FORCE)
    inform_empty_interface(timemory-hello-world "HelloWorld")
    return()
endif()

if(TIMEMORY_USE_HELLO_WORLD)
    # this ensure libtimemory has library in RPATH
    add_rpath(${HelloWorld_LIBRARIES})
    target_link_libraries(timemory-hello-world INTERFACE ${HelloWorld_LIBRARIES})
    target_link_directories(timemory-hello-world INTERFACE ${HelloWorld_LIBRARY_DIRS})
    target_include_directories(timemory-hello-world SYSTEM INTERFACE ${HelloWorld_INCLUDE_DIRS})
    target_compile_definitions(timemory-hello-world INTERFACE TIMEMORY_USE_HELLO_WORLD)
    add_target_flag_if_avail(timemory-hello-world "-any-flags-library-might-need")

    set(NAME hello-world)
    set(DEPS timemory-hello-world)

    file(GLOB_RECURSE header_files ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp)
    file(GLOB_RECURSE source_files ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)

    build_intermediate_library(
        USE_INTERFACE
        NAME                ${NAME}
        TARGET              ${NAME}-component
        CATEGORY            COMPONENT
        FOLDER              components
        HEADERS             ${header_files}
        SOURCES             ${source_files}
        DEPENDS             ${DEPS}
        PROPERTY_DEPENDS    GLOBAL)
endif()

Step 3. source/timemory/components/hello_world/types.hpp

  • Declaration of the component(s)

  • Set any neccesary type-traits

  • Provide a TIMEMORY_PROPERTY_SPECIALIZATION for metadata

    • Will require creating an enumeration ID in enum.h

      • Place your enumerations in the correct alphabetical place

      • Increment TIMEMORY_NATIVE_COMPONENT_ENUM_SIZE by the number of new enum fields

  • Include this file in source/timemory/components/types.hpp

#pragma once

#include "timemory/components/macros.hpp"
#include "timemory/enum.h"
#include "timemory/mpl/type_traits.hpp"
#include "timemory/mpl/types.hpp"

// provide an API tag if there are multiple components
// adds empty struct in tim::api namespace
TIMEMORY_DEFINE_API(hello_world)

// declares that there is a component named hello_world_printer in the tim::component namespace
TIMEMORY_DECLARE_COMPONENT(hello_world_printer)

// marks the API and components as unavailable so that a definition is not needed
#if !defined(TIMEMORY_USE_HELLO_WORLD)
TIMEMORY_DEFINE_CONCRETE_TRAIT(is_available, api::hello_world, false_type)
TIMEMORY_DEFINE_CONCRETE_TRAIT(is_available, component::hello_world_printer, false_type)
#endif

// provides mapping from enumeration to C++ type
// provides mapping from strings to C++ type
// information for timemory-avail
TIMEMORY_PROPERTY_SPECIALIZATION(hello_world_printer, HELLO_WORLD_PRINTER, "hello_world_printer", "")

Step 4. source/timemory/components/hello_world/backends.hpp

Create any entries here that help with portability, e.g. timemory defines the PAPI presets when PAPI is not enabled so that components like papi_tuple<PAPI_L1_TCM> do not cause compile errors when PAPI is not used. Include this file in source/timemory/components/backends.hpp

Step 5. source/timemory/components/hello_world/extern.hpp

  • Provide extern template instantiations, the TIMEMORY_EXTERN_COMPONENT macro is useful here

    • TIMEMORY_EXTERN_COMPONENT(<C++ component type>, <bool for uses storage>, <data type used for storage>)

      • E.g. the wall-clock timer stores 64-bit integers: TIMEMORY_EXTERN_COMPONENT(wall_clock, true, int64_t)

      • E.g. the caliper components handles it’s own data: TIMEMORY_EXTERN_COMPONENT(caliper_marker, false, void)

  • Include this file in source/timemory/components/extern.hpp

#pragma once

#include "timemory/components/base.hpp"
#include "timemory/components/macros.hpp"
#include "timemory/components/hello_world/components.hpp"
#include "timemory/components/hello_world/types.hpp"

// provides minimum declarations or minimum definitions
#if defined(TIMEMORY_USE_EXTERN) && defined(TIMEMORY_USE_COMPONENT_EXTERN)
#    include "timemory/components/extern/declaration.hpp"
#else
#    include "timemory/components/extern/definition.hpp"
#endif

TIMEMORY_EXTERN_COMPONENT(hello_world_printer, false, void)

Step 6. source/timemory/components/hello_world/extern.cpp

Includes the extern.hpp header. TIMEMORY_EXTERN_COMPONENT will generate the instantiation.

#include "timemory/components/hello_world/extern.hpp"

Step 7. source/timemory/components/hello_world/components.hpp

The contents of this file depends on whether the component will be defined internally or externally.

  • If the component is defined externally, include the timemory header your package provides

#pragma once

#if defined(TIMEMORY_USE_HELLO_WORLD)
#    include "hello_world/timemory.hpp"
#endif
  • If the component is defined internally, provide either the declaration or the full definition

    • If your component is not templated and requires linking to an external library, place large member functions in a .cpp file

    • If your component is templated or does not rely on on an external library, the full definition can be placed in the header file

#pragma once

#include "timemory/components/base.hpp"
#include "timemory/components/hello_world/backends.hpp"
#include "timemory/components/hello_world/types.hpp"

#include <cstdio>

namespace tim
{
namespace component
{
struct hello_world_printer : public base<hello_world_printer, void>
{
    using value_type = void;
    using this_type  = hello_world_printer;
    using base_type  = base<this_type, value_type>;

    static std::string label() { return "hello_world_printer"; }
    static std::string description() { return "Prints hello world"; }
    
    void set_prefix(const char* _cstr) { m_prefix = _cstr; }
    
    void start() { printf("Hello World! %s has started", m_prefix); }
    void stop()  { printf("Hello World! %s has stopped", m_prefix); }    
    
private:
    const char* m_prefix = nullptr;
};
}
}

Version and Interface Compatibility

In general, version and interface compatibility issues should be non-existent. Most version and compatibility issues arise from API changes and the changes causing these breaks are almost exclusively due to (1) function signature changes as capabilities evolve and (2) the internal behavior changing in such a way that the extension does not function properly.

Function Signature Changes

Depending on the package, this comes in three general forms: (1) the original function add/removes parameters or changes the return type, e.g. void foo(int) being changed to void foo(int, int), (2) the original function void foo(int) is deprecated and it is suggested  that everyone start using void foo(int, int), or (3) the function signature is opaque (i.e. void*) and there is a new enumeration value for a switch statement which must be handled, e.g. CUpti_ActivityKernel5, etc. In general, timemory is agnostic to return types and parameters being passed to the member functions of a component, it is left entirely up to the component to decide which arguments a member function can handle and which it cannot – if a component wants to disable support for mark_begin(cudaStream_t), then it can just remove the function. If the component gains new capabilities and now requires double get() const to be become std::vector<double> get() const, the component can just make the change and the statistical accumulation will adapt accordingly during compilation.

Behavior changes

This issue tends to arise when internal changes lead to the extension no longer being invoked in the same way as previous versions. There are multiple ways to handle these scenarios and ensure that components retain the desired behavior in the event something changes internally and/or something that was previously available is no longer available. In general, all that is required is a template specialization which provides the desired behavior and doing so will fix that behavior for all previous and future versions of timemory.

For example, suppose if was found that the static polymorphic base was defined in such a way that the data layout could be slightly tweaked to remove one data member for a 50% reduction in overhead and this data member wasn’t used by 99% of the components but your component was part of the 1% using it and your component depended on it. The solution is simple: timemory would make this change and your component would integrate its requirements into the component, possibly even removing the inheritance entirely since components are not required to inherit from a particular base class – the base class only exists to simplify the introduction of new components.

In the scenario where the behavior involves how the component member functions are invoked, all member function invocations are routed through the constructor of a templated operation class or the function operator of that type and these operation classes can be specialized to perform the desired behavior. This feature can be clearly demonstrated by the (fairly unlikely) scenario provided: Foo should call stop() when every other component is calling start() and vice-versa:

namespace tim { 
namespace operation {

template <>
struct start<Foo>
{
    start(Foo& obj)
    {
        // call stop when every other tool calls start
        obj.stop();
    }
};

template <>
struct stop<Foo>
{
    start(Foo& obj)
    {
        // call start when every other tool calls stop
        obj.start();
    }
};
} 
}

Thus, even in the event something changes in timemory and an undesirable behavior is introduced, a component can provide a specialization to eliminate the issue without affecting any other tools and without requiring a change to the component itself.

Higher-level specialization on categories are also possible. All specific operations, such as operation::start<T>, are routed through the constructor of operation::generic_operator<OpT, T, ApiT>. This type is responsible for ensuring that the specific operation (OpT) can be constructed with a reference to T. The primary purpose of this class is to handle when the instance of T is runtime optional, and therefore is a pointer and requires a nullptr check (might not be enabled). But the secondary purpose is to provide an API specialization for higher-level modifications to the behavior of multiple components. Thus, you can disable certain components from being utilized except for when the pre-processor definition TIMEMORY_API is tim::api::SpecialFoo or when the components are part of component_bundle<tim::api::SpecialFoo, ...>:

TIMEMORY_DEFINE_API(SpecialFoo)

namespace tim { 
namespace operation {

// define default behavior of Foo to generally do nothing
template <typename Op, typename Tag>
struct generic_operator<Foo, Op, Tag>
{
    template <typename... Args>
    generic_operator(Args&&... args)
    {}
};

// When the API is SpecialFoo, you do something special
template <typename Tp, typename Op>
struct generic_operator<Tp, Op, api::SpecialFoo>
{
    // implementation of custom handling...
};

Conclusion

Thanks for reading! Please file an issue if there are any remaining question or clarification is required.