Skip to content

Commit 671de1e

Browse files
authored
Merge pull request #35 from ewanwm/feature_instrumentation_profiling
Feature instrumentation profiling
2 parents d51b130 + 26b3dd6 commit 671de1e

19 files changed

+390
-20
lines changed

.github/workflows/CI-build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
- name: Configure CMake
3939
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
4040
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
41-
run: cmake -DTEST_COVERAGE=ON -DCMAKE_PREFIX_PATH=`python3 -c 'import torch;print(torch.utils.cmake_prefix_path)'` -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
41+
run: cmake -DNT_TEST_COVERAGE=ON -DCMAKE_PREFIX_PATH=`python3 -c 'import torch;print(torch.utils.cmake_prefix_path)'` -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
4242

4343
- name: Build
4444
# Build your program with the given configuration

CMakeLists.txt

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ set(CMAKE_CXX_STANDARD 17)
33

44
project(nuTens)
55

6-
OPTION(TEST_COVERAGE "TEST_COVERAGE" OFF)
6+
OPTION(NT_TEST_COVERAGE "produce code coverage reports when running tests" OFF)
77

8-
IF(TEST_COVERAGE)
8+
IF(NT_TEST_COVERAGE)
99
message("Adding flags to check test coverage")
1010
add_compile_options("--coverage")
1111
add_link_options("--coverage")
@@ -22,8 +22,8 @@ CPMAddPackage("gh:gabime/spdlog@1.8.2")
2222

2323
## check build times
2424
## have this optional as it's not supported on all CMake platforms
25-
OPTION(BUILD_TIMING "output time to build each target" OFF)
26-
IF(BUILD_TIMING)
25+
OPTION(NT_BUILD_TIMING "output time to build each target" OFF)
26+
IF(NT_BUILD_TIMING)
2727
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time")
2828
ENDIF()
2929

@@ -33,4 +33,18 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${TORCH_CXX_FLAGS}")
3333

3434

3535
add_subdirectory(nuTens)
36-
add_subdirectory(tests)
36+
add_subdirectory(tests)
37+
38+
39+
message( STATUS "The following variables have been used to configure the build: " )
40+
get_cmake_property(_variableNames VARIABLES)
41+
list (SORT _variableNames)
42+
foreach (_variableName ${_variableNames})
43+
unset(MATCHED)
44+
string(REGEX MATCH "^NT_*" MATCHED ${_variableName})
45+
if (NOT MATCHED)
46+
continue()
47+
endif()
48+
49+
message(STATUS " ${_variableName}=${${_variableName}}")
50+
endforeach()

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ make test
4747
- [x] Doxygen documentation with automatic deployment
4848
- [x] Add test coverage checks into CI
4949
- [x] Integrate linting ( [cpp-linter](https://github.com/cpp-linter)? )
50-
- [ ] Add instrumentation library for benchmarking and profiling
50+
- [x] Add instrumentation library for benchmarking and profiling
5151
- [ ] Add suite of benchmarking tests
5252
- [ ] Integrate benchmarks into CI ( maybe use [hyperfine](https://github.com/sharkdp/hyperfine) and [bencher](https://bencher.dev/) for this? )
5353
- [ ] Add proper unit tests

nuTens/CMakeLists.txt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11

2-
3-
## set up logging
2+
########################
3+
#### set up logging ####
4+
########################
45
add_library(logging logging.hpp)
56
target_link_libraries(logging spdlog::spdlog)
67
set_target_properties(logging PROPERTIES LINKER_LANGUAGE CXX)
78

8-
set( LOG_LEVEL "INFO" CACHE STRING "the level of detail to log to the console" )
9+
set( NT_LOG_LEVEL "INFO" CACHE STRING "the level of detail to log to the console" )
910

10-
## Convert LOG_LEVEL to all upper case so that we aren't case sensitive to user input
11-
string( TOUPPER "${LOG_LEVEL}" LOG_LEVEL_UPPER )
11+
## Convert NT_LOG_LEVEL to all upper case so that we aren't case sensitive to user input
12+
string( TOUPPER "${NT_LOG_LEVEL}" LOG_LEVEL_UPPER )
1213

1314
## Check the specified log level is valid
1415
set(VALID_LOG_OPTIONS SILENT ERROR WARNING INFO DEBUG TRACE)
@@ -24,17 +25,28 @@ target_compile_definitions(logging PUBLIC NT_LOG_LEVEL=NT_LOG_LEVEL_${LOG_LEVEL_
2425

2526

2627

28+
################################
29+
#### set up instrumentation ####
30+
################################
31+
add_library(instrumentation instrumentation.hpp)
32+
set_target_properties(instrumentation PROPERTIES LINKER_LANGUAGE CXX)
33+
34+
option( NT_PROFILING "enable profiling of the code" OFF )
35+
if( NT_PROFILING )
36+
target_compile_definitions( instrumentation PUBLIC USE_PROFILING )
37+
endif()
38+
2739

2840
## if user wants to use pch then we use the pch
2941
## people, especially if developing, might want to use this as including tensor related things
3042
## can be excruciatingly sloow when building
31-
OPTION(USE_PCH "USE_PCH" OFF)
32-
IF(USE_PCH)
43+
OPTION(NT_USE_PCH "NT_USE_PCH" OFF)
44+
IF(NT_USE_PCH)
3345
message("Using precompiled header")
3446

3547
add_library(nuTens-pch nuTens-pch.hpp)
3648

37-
SET(PCH_LIBS "${PCH_LIBS};logging")
49+
SET(PCH_LIBS "${PCH_LIBS};logging;instrumentation")
3850

3951
## the headers included in the PCH will (at some point) depend on which tensor library is being used
4052
IF(TORCH_FOUND)
@@ -50,7 +62,7 @@ IF(USE_PCH)
5062
target_precompile_headers(nuTens-pch PUBLIC nuTens-pch.hpp)
5163
set_target_properties(nuTens-pch PROPERTIES LINKER_LANGUAGE CXX)
5264

53-
ENDIF() ## end USE_PCH block
65+
ENDIF() ## end NT_USE_PCH block
5466

5567

5668

nuTens/instrumentation.hpp

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <chrono>
5+
#include <fstream>
6+
#include <string>
7+
#include <thread>
8+
#include <utility>
9+
/*! \file instrumentation.hpp
10+
\brief Define utilities for instrumentation of the code
11+
12+
This is the home of anything that gets placed inside other classes or functions in order to instrument the code.
13+
e.g. for profiling or debugging. Everything should ideally be macro-fied so it can be included only for certain
14+
builds, or specified by build time options.
15+
*/
16+
17+
struct ProfileResult
18+
{
19+
/// @brief Hold the results of a profiled function to be written out.
20+
21+
std::string name;
22+
long start;
23+
long end;
24+
uint32_t threadID;
25+
};
26+
27+
class ProfileWriter
28+
{
29+
/*! @class ProfileWriter
30+
* @brief Singleton class to collect timing information for functions and write out to a file that can be inspected
31+
* later with visual profiling tool
32+
*
33+
* Writes out profiling information in a json format readable by chrome tracing
34+
* (https://www.chromium.org/developers/how-tos/trace-event-profiling-tool/) Use the macros provided to instrument
35+
* the code like: \code{.cpp}
36+
*
37+
* \code{.cpp}
38+
*
39+
* Then open up your favourite chromium based browser and go to chrome://tracing. You can then just drag and drop
40+
* the profiling json file and should see a lovely display of the collected profile information.
41+
*/
42+
43+
/// \todo currently only suppor the format used by chrome tracing. Would be nice to support other formats too.
44+
/// Should just be a case of adding additional option for writeProfile and header and footer
45+
46+
public:
47+
/// @brief Constructor
48+
ProfileWriter() = default;
49+
50+
/// @brief Set up the session
51+
/// @param[in] name The name of the timer
52+
/// @param[in] filePath The destination of the output file
53+
void beginSession(const std::string &name, const std::string &filePath = "results.json")
54+
{
55+
_outputStream.open(filePath);
56+
writeHeader();
57+
_name = name;
58+
}
59+
60+
/// @brief Close the session and clean up
61+
void endSession()
62+
{
63+
writeFooter();
64+
_outputStream.close();
65+
_name = "";
66+
_profileCount = 0;
67+
}
68+
69+
/// @brief Write out the results of a profiled function
70+
/// @param[in] result The result to write
71+
void writeProfile(const ProfileResult &result)
72+
{
73+
if (_profileCount++ > 0)
74+
{
75+
_outputStream << ",";
76+
}
77+
78+
std::string name = result.name;
79+
std::replace(name.begin(), name.end(), '"', '\'');
80+
81+
_outputStream << "{";
82+
_outputStream << R"("cat":"function",)";
83+
_outputStream << "\"dur\":" << (result.end - result.start) << ',';
84+
_outputStream << R"("name":")" << name << "\",";
85+
_outputStream << R"("ph":"X",)";
86+
_outputStream << "\"pid\":0,";
87+
_outputStream << "\"tid\":" << result.threadID << ",";
88+
_outputStream << "\"ts\":" << result.start;
89+
_outputStream << "}";
90+
91+
_outputStream.flush();
92+
}
93+
94+
/// @brief Write the file header
95+
void writeHeader()
96+
{
97+
_outputStream << R"({"otherData": {},"traceEvents":[)";
98+
_outputStream.flush();
99+
}
100+
101+
/// @brief Write the file footer
102+
void writeFooter()
103+
{
104+
_outputStream << "]}";
105+
_outputStream.flush();
106+
}
107+
108+
/// @brief Get a reference to the ProfileWriter, if it has not yet been instantiated, this will do so
109+
static ProfileWriter &get()
110+
{
111+
static ProfileWriter instance; // this will be instantiated the first time ProfileWriter::get() is called and
112+
// killed at the end of the program
113+
return instance;
114+
}
115+
116+
private:
117+
std::string _name;
118+
std::ofstream _outputStream;
119+
uint _profileCount{0};
120+
};
121+
122+
class InstrumentationTimer
123+
/*!
124+
* @class InstrumentationTimer
125+
* @brief Class to perform the actual timing
126+
*
127+
*
128+
*
129+
*/
130+
{
131+
public:
132+
/// @brief Construct an InstrumentationTimer object and start the clock
133+
/// @param[in] name The name of the profile. Typically use __FUNCSIG__ so it's clear which part of the code is being
134+
/// profiled.
135+
InstrumentationTimer(std::string name) : _name(std::move(name)), _stopped(false)
136+
{
137+
_startTimepoint = std::chrono::high_resolution_clock::now();
138+
}
139+
140+
/// @brief Destroy the timer object and stop the timer by calling stop()
141+
~InstrumentationTimer()
142+
{
143+
if (!_stopped)
144+
{
145+
stop();
146+
}
147+
}
148+
149+
InstrumentationTimer(const InstrumentationTimer &) = delete;
150+
/// @brief Stop the timer and write out the profile result using the ProfileWriter
151+
void stop()
152+
{
153+
auto endTimepoint = std::chrono::high_resolution_clock::now();
154+
155+
long long start =
156+
std::chrono::time_point_cast<std::chrono::microseconds>(_startTimepoint).time_since_epoch().count();
157+
long long end =
158+
std::chrono::time_point_cast<std::chrono::microseconds>(endTimepoint).time_since_epoch().count();
159+
160+
uint32_t threadID = std::hash<std::thread::id>{}(std::this_thread::get_id());
161+
ProfileWriter::get().writeProfile({_name, start, end, threadID});
162+
163+
_stopped = true;
164+
}
165+
166+
private:
167+
std::string _name;
168+
std::chrono::time_point<std::chrono::high_resolution_clock> _startTimepoint;
169+
bool _stopped;
170+
};
171+
172+
/// @brief Begin a profiling session
173+
/// Will open up the results json file and set things up.
174+
/// If USE_PROFILING not defined will be empty so that it can be stripped from non-debug builds
175+
/// @param[in] sessionName The name of the session
176+
#ifdef USE_PROFILING
177+
// NOLINTNEXTLINE
178+
#define NT_PROFILE_BEGINSESSION(sessionName) \
179+
ProfileWriter::get().beginSession(sessionName, std::string(sessionName) + "-profile.json")
180+
#else
181+
#define NT_PROFILE_BEGINSESSION(sessionName)
182+
#endif
183+
184+
/// @brief Profile the current scope
185+
#ifdef USE_PROFILING
186+
// NOLINTNEXTLINE
187+
#define NT_PROFILE() InstrumentationTimer timer##__LINE__(std::string(__PRETTY_FUNCTION__))
188+
#else
189+
#define NT_PROFILE()
190+
#endif
191+
192+
/// @brief End the profiling session
193+
#ifdef USE_PROFILING
194+
// NOLINTNEXTLINE
195+
#define NT_PROFILE_ENDSESSION() ProfileWriter::get().endSession()
196+
#else
197+
#define NT_PROFILE_ENDSESSION()
198+
#endif

nuTens/nuTens-pch.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <complex>
77
#include <iostream>
88
#include <map>
9+
#include <nuTens/instrumentation.hpp>
910
#include <nuTens/logging.hpp>
1011
#include <variant>
1112
#include <vector>

nuTens/propagator/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ IF(USE_PCH)
1313
target_precompile_headers(propagator REUSE_FROM tensor)
1414
ENDIF()
1515

16-
target_link_libraries(propagator PUBLIC tensor constants)
16+
target_link_libraries(propagator PUBLIC tensor constants instrumentation)
1717

1818
target_include_directories(propagator PUBLIC "${CMAKE_SOURCE_DIR}")
1919
set_target_properties(propagator PROPERTIES LINKER_LANGUAGE CXX)

nuTens/propagator/base-matter-solver.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <nuTens/instrumentation.hpp>
34
#include <nuTens/tensors/tensor.hpp>
45

56
class BaseMatterSolver

nuTens/propagator/const-density-solver.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
void ConstDensityMatterSolver::calculateEigenvalues(const Tensor &energies, Tensor &eigenvectors, Tensor &eigenvalues)
44
{
5+
NT_PROFILE();
56
Tensor hamiltonian;
67
hamiltonian.zeros({energies.getBatchDim(), nGenerations, nGenerations}, NTdtypes::kComplexFloat);
78

nuTens/propagator/const-density-solver.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ class ConstDensityMatterSolver : public BaseMatterSolver
4242

4343
/// @brief Set a new PMNS matrix for this solver
4444
/// @param newPMNS The new matrix to set
45-
inline void setPMNS(const Tensor &newPMNS)
45+
inline void setPMNS(const Tensor &newPMNS) override
4646
{
47+
NT_PROFILE();
4748
PMNS = newPMNS;
4849

4950
// construct the outer product of the electron neutrino row of the PMNS
@@ -54,8 +55,9 @@ class ConstDensityMatterSolver : public BaseMatterSolver
5455

5556
/// @brief Set new mass eigenvalues for this solver
5657
/// @param newMasses The new masses
57-
inline void setMasses(const Tensor &newMasses)
58+
inline void setMasses(const Tensor &newMasses) override
5859
{
60+
NT_PROFILE();
5961
masses = newMasses;
6062

6163
// construct the diagonal mass^2 matrix used in the hamiltonian

nuTens/propagator/propagator.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Tensor Propagator::calculateProbs(const Tensor &energies) const
44
{
5+
NT_PROFILE();
6+
57
Tensor ret;
68

79
// if a matter solver was specified, use effective values for masses and PMNS
@@ -27,6 +29,8 @@ Tensor Propagator::calculateProbs(const Tensor &energies) const
2729

2830
Tensor Propagator::_calculateProbs(const Tensor &energies, const Tensor &massesSq, const Tensor &PMNS) const
2931
{
32+
NT_PROFILE();
33+
3034
Tensor weightMatrix;
3135
weightMatrix.ones({energies.getBatchDim(), _nGenerations, _nGenerations}, NTdtypes::kComplexFloat)
3236
.requiresGrad(false);

0 commit comments

Comments
 (0)