Skip to content

Commit 39fc1b4

Browse files
authored
Merge pull request #2409 from SCIInstitute/profiler
Add profiling/tracing module and speed improvments to correspondence function of optimizer
2 parents 25a082f + d398a6d commit 39fc1b4

File tree

18 files changed

+634
-90
lines changed

18 files changed

+634
-90
lines changed

Applications/shapeworks/Commands.cpp

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

1212
#include <boost/filesystem.hpp>
1313

14+
#include <Profiling.h>
15+
1416
namespace shapeworks {
1517

1618
// boilerplate for a command. Copy this to start a new command
@@ -119,10 +121,12 @@ bool OptimizeCommand::execute(const optparse::Values& options, SharedCommandData
119121
project->set_filename(StringUtils::getFilename(projectFile));
120122
}
121123

124+
TIME_START("set_up_optimize");
122125
// set up Optimize class based on project parameters
123126
OptimizeParameters params(project);
124127
params.set_up_optimize(&app);
125128
app.SetProject(project);
129+
TIME_STOP("set_up_optimize");
126130

127131
bool success = app.Run();
128132

Applications/shapeworks/shapeworks.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <exception>
66
#include "Executable.h"
77
#include "Commands.h"
8+
#include <Profiling.h>
89

910
using namespace shapeworks;
1011

@@ -111,9 +112,14 @@ int main(int argc, char *argv[])
111112
shapeworks.addCommand(ConvertProjectCommand::getCommand());
112113

113114
try {
114-
return shapeworks.run(argc, argv);
115+
TIME_START("shapeworks");
116+
int rc = shapeworks.run(argc, argv);
117+
TIME_STOP("shapeworks");
118+
TIME_FINALIZE();
119+
return rc;
115120
} catch (const std::exception &e) {
116121
std::cout << e.what() << std::endl;
122+
TIME_FINALIZE();
117123
return EXIT_FAILURE;
118124
}
119125
}

Libs/Common/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ set(Common_sources
44
ShapeworksUtils.cpp
55
Region.cpp
66
Logging.cpp
7+
Profiling.cpp
78
)
89
set(Common_headers
910
Shapeworks.h
1011
ShapeworksUtils.h
1112
Region.h
1213
Logging.h
14+
Profiling.h
1315
)
1416
add_library(Common STATIC
1517
${Common_sources}

Libs/Common/Logging.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,16 @@ void Logging::log_stack(const std::string& message) const {
8787

8888
//-----------------------------------------------------------------------------
8989
void Logging::log_error(const std::string& message, const int line, const char* file) const {
90-
spd::error(message);
90+
std::string str = message;
91+
if (spd::get_level() == spd::level::debug) {
92+
str = create_header(line, file) + " Error: " + message;
93+
}
94+
spd::error(str);
9195
if (log_open_) {
92-
spd::get("file")->error(message);
96+
spd::get("file")->error(str);
9397
}
9498
if (error_callback_) {
95-
error_callback_(message);
99+
error_callback_(str);
96100
}
97101
}
98102

Libs/Common/Profiling.cpp

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
#include "Profiling.h"
2+
3+
#include <QCoreApplication>
4+
#include <QDebug>
5+
#include <QDir>
6+
#include <QFile>
7+
#include <QJsonArray>
8+
#include <QJsonDocument>
9+
#include <QJsonObject>
10+
#include <QStandardPaths>
11+
#include <QTextStream>
12+
#include <QThread>
13+
#include <algorithm>
14+
#include <cstdlib>
15+
#include <iostream>
16+
17+
namespace shapeworks {
18+
19+
//---------------------------------------------------------------------------
20+
TimerStackEntry::TimerStackEntry(const QString& name, qint64 start_time_us)
21+
: name(name), start_time_us(start_time_us), accumulated_child_time_ms(0.0) {}
22+
23+
//---------------------------------------------------------------------------
24+
Profiler::Profiler() : profiling_enabled_(false), tracing_enabled_(false), start_time_us_(0) {
25+
// Check environment variables
26+
const char* profile_env = std::getenv("SW_TIME_PROFILE");
27+
if (profile_env) {
28+
QString profile_str = QString(profile_env).toLower();
29+
profiling_enabled_ = (profile_str == "on" || profile_str == "1" || profile_str == "true");
30+
}
31+
32+
const char* trace_env = std::getenv("SW_TIME_TRACE");
33+
if (trace_env) {
34+
QString trace_str = QString(trace_env).toLower();
35+
tracing_enabled_ = (trace_str == "on" || trace_str == "1" || trace_str == "true");
36+
}
37+
38+
if (profiling_enabled_ || tracing_enabled_) {
39+
elapsed_timer_.start();
40+
start_time_us_ = std::chrono::duration_cast<std::chrono::microseconds>(
41+
std::chrono::high_resolution_clock::now().time_since_epoch())
42+
.count();
43+
}
44+
}
45+
46+
//---------------------------------------------------------------------------
47+
Profiler::~Profiler() {}
48+
49+
//---------------------------------------------------------------------------
50+
Profiler& Profiler::instance() {
51+
static Profiler instance;
52+
return instance;
53+
}
54+
55+
//---------------------------------------------------------------------------
56+
void Profiler::start_timer(const QString& name) {
57+
if (!profiling_enabled_ && !tracing_enabled_) {
58+
return;
59+
}
60+
61+
QMutexLocker locker(&mutex_);
62+
Qt::HANDLE thread_id = QThread::currentThreadId();
63+
qint64 current_time_us = elapsed_timer_.nsecsElapsed() / 1000;
64+
65+
// Add to timer stack
66+
auto& stack = timer_stacks_[thread_id];
67+
stack.push_back(std::make_unique<TimerStackEntry>(name, current_time_us));
68+
69+
// Add trace event if tracing enabled
70+
if (tracing_enabled_) {
71+
TraceEvent event;
72+
event.name = name;
73+
event.phase = "B";
74+
event.timestamp_us = start_time_us_ + current_time_us;
75+
event.thread_id = thread_id;
76+
event.process_id = QCoreApplication::applicationPid();
77+
trace_events_.push_back(event);
78+
}
79+
}
80+
81+
//---------------------------------------------------------------------------
82+
void Profiler::stop_timer(const QString& name) {
83+
if (!profiling_enabled_ && !tracing_enabled_) {
84+
return;
85+
}
86+
87+
QMutexLocker locker(&mutex_);
88+
Qt::HANDLE thread_id = QThread::currentThreadId();
89+
qint64 current_time_us = elapsed_timer_.nsecsElapsed() / 1000;
90+
91+
auto& stack = timer_stacks_[thread_id];
92+
if (stack.empty()) {
93+
qWarning() << "Timer stack underflow for" << name;
94+
return;
95+
}
96+
97+
// Find matching timer in stack (should be at top, but handle mismatched calls)
98+
auto it = std::find_if(stack.rbegin(), stack.rend(),
99+
[&name](const std::unique_ptr<TimerStackEntry>& entry) { return entry->name == name; });
100+
101+
if (it == stack.rend()) {
102+
qWarning() << "No matching start timer found for" << name;
103+
return;
104+
}
105+
106+
auto& entry = **it;
107+
double elapsed_ms = (current_time_us - entry.start_time_us) / 1000.0;
108+
109+
if (profiling_enabled_) {
110+
// Update profile entry
111+
auto& profile_entry = profile_entries_[name];
112+
profile_entry.name = name;
113+
profile_entry.thread_id = thread_id;
114+
profile_entry.call_count++;
115+
profile_entry.inclusive_time_ms += elapsed_ms;
116+
profile_entry.exclusive_time_ms += (elapsed_ms - entry.accumulated_child_time_ms);
117+
118+
// Update parent's subcall count and child time
119+
if (stack.size() > 1) {
120+
auto parent_it = stack.end() - 2;
121+
if (std::distance(stack.begin(), parent_it) >= 0) {
122+
(*parent_it)->accumulated_child_time_ms += elapsed_ms;
123+
auto& parent_profile = profile_entries_[(*parent_it)->name];
124+
parent_profile.subcall_count++;
125+
}
126+
}
127+
}
128+
129+
// Add trace event if tracing enabled
130+
if (tracing_enabled_) {
131+
TraceEvent event;
132+
event.name = name;
133+
event.phase = "E";
134+
event.timestamp_us = start_time_us_ + current_time_us;
135+
event.thread_id = thread_id;
136+
event.process_id = QCoreApplication::applicationPid();
137+
trace_events_.push_back(event);
138+
}
139+
140+
// Remove from stack
141+
stack.erase(it.base() - 1);
142+
}
143+
144+
//---------------------------------------------------------------------------
145+
void Profiler::finalize() {
146+
if (!profiling_enabled_ && !tracing_enabled_) {
147+
return;
148+
}
149+
150+
QMutexLocker locker(&mutex_);
151+
152+
if (profiling_enabled_) {
153+
write_profile_report();
154+
}
155+
156+
if (tracing_enabled_) {
157+
write_trace_file();
158+
}
159+
}
160+
void Profiler::write_profile_report() {
161+
// Calculate total time
162+
double total_time_ms = elapsed_timer_.elapsed();
163+
164+
// Group entries by thread
165+
std::unordered_map<Qt::HANDLE, std::vector<ProfileEntry*>> entries_by_thread;
166+
for (auto& pair : profile_entries_) {
167+
entries_by_thread[pair.second.thread_id].push_back(&pair.second);
168+
}
169+
170+
QString report;
171+
QTextStream stream(&report);
172+
173+
int tid = 0;
174+
for (const auto& thread_pair : entries_by_thread) {
175+
Qt::HANDLE thread_id = thread_pair.first;
176+
auto entries = thread_pair.second;
177+
178+
// Sort by inclusive time (descending)
179+
std::sort(entries.begin(), entries.end(),
180+
[](const ProfileEntry* a, const ProfileEntry* b) { return a->inclusive_time_ms > b->inclusive_time_ms; });
181+
182+
stream << QString("THREAD %1: ").arg(tid++) << "\n";
183+
stream
184+
<< "------------------------------------------------------------------------------------------------------\n";
185+
stream << QString("%1 %2 %3 %4 %5 %6 %7 %8\n")
186+
.arg(" %Time", 8)
187+
.arg("Exclusive", 12)
188+
.arg("Inclusive", 12)
189+
.arg("#Calls", 8)
190+
.arg("#Child", 8)
191+
.arg("Exclusive", 12)
192+
.arg("Inclusive", 12)
193+
.arg("Name");
194+
stream << QString("%1 %2 %3 %4 %5 %6 %7 %8\n")
195+
.arg("", 8)
196+
.arg("total ms", 12)
197+
.arg("total ms", 12)
198+
.arg("", 8)
199+
.arg("", 8)
200+
.arg("ms/call", 12)
201+
.arg("ms/call", 12)
202+
.arg("");
203+
stream
204+
<< "------------------------------------------------------------------------------------------------------\n";
205+
206+
for (const auto* entry : entries) {
207+
double percent = (entry->inclusive_time_ms / total_time_ms) * 100.0;
208+
double exclusive_msec_per_call = entry->call_count > 0 ? entry->exclusive_time_ms / entry->call_count : 0.0;
209+
double inclusive_msec_per_call = entry->call_count > 0 ? entry->inclusive_time_ms / entry->call_count : 0.0;
210+
211+
stream << QString("%1 %2 %3 %4 %5 %6 %7 %8\n")
212+
.arg(QString::number(percent, 'f', 1), 8)
213+
.arg(QString::number(entry->exclusive_time_ms, 'f', 0), 12)
214+
.arg(QString::number(entry->inclusive_time_ms, 'f', 0), 12)
215+
.arg(entry->call_count, 8)
216+
.arg(entry->subcall_count, 8)
217+
.arg(QString::number(exclusive_msec_per_call, 'f', 0), 12)
218+
.arg(QString::number(inclusive_msec_per_call, 'f', 0), 12)
219+
.arg(entry->name);
220+
}
221+
stream
222+
<< "------------------------------------------------------------------------------------------------------\n\n";
223+
}
224+
225+
// Output to console
226+
std::cout << report.toStdString() << std::endl;
227+
228+
// Write to file
229+
QFile file("profile.txt");
230+
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
231+
QTextStream file_stream(&file);
232+
file_stream << report;
233+
file.close();
234+
std::cout << "Profile written to profile.txt" << std::endl;
235+
} else {
236+
qWarning() << "Failed to write profile.txt";
237+
}
238+
}
239+
//---------------------------------------------------------------------------
240+
void Profiler::write_trace_file() {
241+
QJsonObject root;
242+
QJsonArray events;
243+
244+
for (const auto& event : trace_events_) {
245+
QJsonObject json_event;
246+
json_event["name"] = event.name;
247+
json_event["ph"] = event.phase;
248+
json_event["ts"] = event.timestamp_us;
249+
json_event["tid"] = static_cast<qint64>(reinterpret_cast<quintptr>(event.thread_id));
250+
json_event["pid"] = event.process_id;
251+
events.append(json_event);
252+
}
253+
254+
root["traceEvents"] = events;
255+
256+
QJsonDocument doc(root);
257+
258+
QFile file("trace.json");
259+
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
260+
file.write(doc.toJson());
261+
file.close();
262+
std::cout << "Trace written to trace.json" << std::endl;
263+
} else {
264+
qWarning() << "Failed to write trace.json";
265+
}
266+
}
267+
268+
//---------------------------------------------------------------------------
269+
QString Profiler::format_time(double ms) {
270+
if (ms >= 1000.0) {
271+
return QString::number(ms / 1000.0, 'f', 3) + "s";
272+
} else {
273+
return QString::number(ms, 'f', 3) + "ms";
274+
}
275+
}
276+
277+
//---------------------------------------------------------------------------
278+
ScopedTimer::ScopedTimer(const QString& name)
279+
: name_(name), enabled_(Profiler::instance().is_profiling_enabled() || Profiler::instance().is_tracing_enabled()) {
280+
if (enabled_) {
281+
Profiler::instance().start_timer(name_);
282+
}
283+
}
284+
285+
//---------------------------------------------------------------------------
286+
ScopedTimer::~ScopedTimer() {
287+
if (enabled_) {
288+
Profiler::instance().stop_timer(name_);
289+
}
290+
}
291+
292+
} // namespace shapeworks

0 commit comments

Comments
 (0)