Skip to content

Commit

Permalink
Add --cpu-stats flag
Browse files Browse the repository at this point in the history
Add a `--cpu-stats` flag that will print out timing information for
any interesting and critical parts of the code for future investigation.
This is similar to Ninja's `-d stats` mode.

Rename `--memory` to `--memory-stats` as this is a better name that
better conveys its meaning.
  • Loading branch information
elliotgoodrich committed Nov 7, 2024
1 parent 2ac205e commit f854fd3
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 12 deletions.
28 changes: 21 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ add_executable(
src/allocationprofiler.cpp
src/basicscope.cpp
src/builddirutil.cpp
src/cpuprofiler.cpp
src/depsreader.cpp
src/edgescope.cpp
src/evalstring.cpp
Expand Down Expand Up @@ -193,33 +194,46 @@ set_tests_properties(
PROPERTIES FIXTURES_REQUIRED trimja.snapshot.simple.fixture
)

# --memory
# --memory-stats
if(WIN32)
add_test(
NAME trimja.--memory
NAME trimja.--memory-stats
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/passthrough/
COMMAND trimja
--memory=3
--memory-stats=3
--affected changed.txt
)
set_tests_properties(
trimja.--memory
trimja.--memory-stats
PROPERTIES FIXTURES_REQUIRED trimja.snapshot.passthrough.fixture
)

add_test(
NAME trimja.--memory2
NAME trimja.--memory-stats2
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/builddir/
COMMAND trimja
--memory=3
--memory-stats=3
--builddir
)
set_tests_properties(
trimja.--memory2
trimja.--memory-stats2
PROPERTIES FIXTURES_REQUIRED trimja.snapshot.builddir.fixture
)
endif()

# --cpu-stats
add_test(
NAME trimja.--cpu-stats
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests/passthrough/
COMMAND trimja
--cpu-stats
--affected changed.txt
)
set_tests_properties(
trimja.--cpu-stats
PROPERTIES FIXTURES_REQUIRED trimja.snapshot.passthrough.fixture
)

# --builddir
add_test(
NAME trimja.--builddir
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Options:
-w, --write overwrite input ninja build file
--explain print why each part of the build file was kept
--builddir print the $builddir variable relative to the cwd
--memory=N print memory stats and top N allocating functions
--memory-stats=N print memory stats and top N allocating functions
--cpu-stats print timing stats
-h, --help print help
-v, --version print trimja version
Expand Down
6 changes: 5 additions & 1 deletion src/builddirutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "builddirutil.h"

#include "basicscope.h"
#include "cpuprofiler.h"
#include "manifestparser.h"

#include <fstream>
Expand Down Expand Up @@ -119,7 +120,10 @@ std::filesystem::path BuildDirUtil::builddir(
const std::filesystem::path& ninjaFile,
const std::string& ninjaFileContents) {
BuildDirContext ctx;
ctx.parse(ninjaFile, ninjaFileContents);
{
const Timer t = CPUProfiler::start(".ninja parse");
ctx.parse(ninjaFile, ninjaFileContents);
}
std::string builddir;
ctx.fileScope.appendValue(builddir, "builddir");
return std::filesystem::path(ninjaFile).remove_filename() / builddir;
Expand Down
77 changes: 77 additions & 0 deletions src/cpuprofiler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// MIT License
//
// Copyright (c) 2024 Elliot Goodrich
//
// 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.

#include "cpuprofiler.h"

#include <list>
#include <ostream>
#include <string>

namespace trimja {

namespace {

std::list<std::pair<std::string, std::chrono::steady_clock::duration>>
g_cpuMetrics;
bool g_cpuEnabled = false;

} // namespace

Timer::Timer(std::chrono::steady_clock::duration* output) : m_output{output} {
if (m_output) {
m_start = std::chrono::steady_clock::now();
}
}

Timer::~Timer() {
stop();
}

void Timer::stop() {
if (m_output) {
*m_output = std::chrono::steady_clock::now() - m_start;
}
}

void CPUProfiler::enable() {
g_cpuEnabled = true;
}

bool CPUProfiler::isEnabled() {
return g_cpuEnabled;
}

Timer CPUProfiler::start(std::string_view name) {
return Timer{g_cpuEnabled ? &g_cpuMetrics.emplace_back(name, 0).second
: nullptr};
}

void CPUProfiler::print(std::ostream& out) {
for (const auto& [name, duration] : g_cpuMetrics) {
out << name << ": "
<< std::chrono::duration_cast<std::chrono::microseconds>(duration)
.count()
<< "us\n";
}
}

} // namespace trimja
92 changes: 92 additions & 0 deletions src/cpuprofiler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// MIT License
//
// Copyright (c) 2024 Elliot Goodrich
//
// 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.

#ifndef TRIMJA_CPUPROFILER
#define TRIMJA_CPUPROFILER

#include <chrono>
#include <iosfwd>
#include <string_view>

namespace trimja {

/**
* @brief Timer class to measure the duration of code execution.
*/
struct Timer {
std::chrono::steady_clock::duration* m_output;
std::chrono::steady_clock::time_point m_start;

/**
* @brief Constructs a Timer and optionally starts timing.
*
* @param output Optional pointer to a duration object where the elapsed time
* will be stored if it is not nullptr.
*/
Timer(std::chrono::steady_clock::duration* output);

/**
* @brief Destroys the Timer and stops timing if not already stopped.
*/
~Timer();

/**
* @brief Stops the timer and writes the elapsed time.
*/
void stop();
};

/**
* @brief Utility to enable and manage CPU profiling.
*/
struct CPUProfiler {
/**
* @brief Enables the CPU profiler.
*/
static void enable();

/**
* @brief Checks if the CPU profiler is enabled.
*
* @return Whether the profiler is enabled.
*/
static bool isEnabled();

/**
* @brief Starts a new timer for profiling a specific code section.
*
* @param name The name of the code section being profiled.
* @return A Timer object that measures the duration of the code section.
*/
static Timer start(std::string_view name);

/**
* @brief Prints the profiling results to the provided output stream.
*
* @param out The output stream to print the profiling results.
*/
static void print(std::ostream& out);
};

} // namespace trimja

#endif
15 changes: 13 additions & 2 deletions src/trimja.m.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include "allocationprofiler.h"
#include "builddirutil.h"
#include "cpuprofiler.h"
#include "trimutil.h"

#include <ninja/getopt.h>
Expand Down Expand Up @@ -73,9 +74,10 @@ work as expected.
--builddir print the $builddir variable relative to the cwd)HELP"
#if WIN32
R"HELP(
--memory=N print memory stats and top N allocating functions)HELP"
--memory-stats=N print memory stats and top N allocating functions)HELP"
#endif
R"HELP(
--cpu-stats print timing stats
-h, --help print help
-v, --version print trimja version ()HELP" TRIMJA_VERSION
R"HELP()
Expand Down Expand Up @@ -105,7 +107,8 @@ static const option g_longOptions[] = {
{"affected", required_argument, nullptr, 'a'},
{"version", no_argument, nullptr, 'v'},
{"write", no_argument, nullptr, 'w'},
{"memory", required_argument, nullptr, 'm'},
{"memory-stats", required_argument, nullptr, 'm'},
{"cpu-stats", no_argument, nullptr, 'u'},
{},
};

Expand All @@ -117,6 +120,10 @@ bool instrumentMemory = false;
trimja::AllocationProfiler::print(std::cerr, topAllocatingStacks);
std::cerr.flush();
}
if (trimja::CPUProfiler::isEnabled()) {
trimja::CPUProfiler::print(std::cerr);
std::cerr.flush();
}
std::_Exit(rc);
};

Expand Down Expand Up @@ -244,6 +251,9 @@ bool instrumentMemory = false;
leave(EXIT_FAILURE);
}
break;
case 'u':
CPUProfiler::enable();
break;
case '?':
std::cerr << "Unknown option" << std::endl;
leave(EXIT_FAILURE);
Expand All @@ -256,6 +266,7 @@ bool instrumentMemory = false;
// Copy everything from the ninja into a stringstream in case we
// are overwriting the same input file
const std::string ninjaFileContents = [&] {
const Timer ninjaRead = CPUProfiler::start(".ninja read");
std::stringstream ninjaCopy;
std::ifstream ninja(ninjaFile);
ninjaCopy << ninja.rdbuf();
Expand Down
11 changes: 10 additions & 1 deletion src/trimutil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "trimutil.h"

#include "basicscope.h"
#include "cpuprofiler.h"
#include "depsreader.h"
#include "edgescope.h"
#include "evalstring.h"
Expand Down Expand Up @@ -862,7 +863,10 @@ void TrimUtil::trim(std::ostream& output,

// Parse the build file, this needs to be the first thing so we choose the
// canonical paths in the same way that ninja does
ctx.parse(ninjaFile, ninjaFileContents);
{
const Timer t = CPUProfiler::start(".ninja parse");
ctx.parse(ninjaFile, ninjaFileContents);
}

Graph& graph = ctx.graph;

Expand All @@ -881,6 +885,7 @@ void TrimUtil::trim(std::ostream& output,
// Add all dynamic dependencies from `.ninja_deps` to the graph
if (const std::filesystem::path ninjaDeps = builddir / ".ninja_deps";
std::filesystem::exists(ninjaDeps)) {
const Timer t = CPUProfiler::start(".ninja_deps parse");
parseDepFile(ninjaDeps, graph, ctx);
}

Expand All @@ -900,6 +905,7 @@ void TrimUtil::trim(std::ostream& output,
}
isAffected.assign(isAffected.size(), true);
} else {
const Timer t = CPUProfiler::start(".ninja_log parse");
parseLogFile(
ninjaLog, ctx, isAffected,
[&](const std::size_t index) {
Expand Down Expand Up @@ -989,6 +995,7 @@ void TrimUtil::trim(std::ostream& output,
std::vector<bool> seen(graph.size());

// Mark all outputs that have an affected input as affected
Timer trimTimer = CPUProfiler::start("trim time");
for (std::size_t index = 0; index < graph.size(); ++index) {
markIfChildrenAffected(index, seen, isAffected, ctx, explain);
}
Expand Down Expand Up @@ -1059,7 +1066,9 @@ void TrimUtil::trim(std::ostream& output,
[&](std::size_t index) { ctx.parts[index] = ""; });
}
}
trimTimer.stop();

const Timer writeTimer = CPUProfiler::start("output time");
std::copy(ctx.parts.begin(), ctx.parts.end(),
std::ostream_iterator<std::string_view>(output));
}
Expand Down

0 comments on commit f854fd3

Please sign in to comment.